What i have stumped across is that when i pass a Binding<> as a parameter in a ViewModel as you will see below, it doesn't always work as intended. It works in iOS 15 but not in iOS 16. This reduces the capability to create subviews to handle this without sending the entire Holder object as a #Binding down to it's subview, which i would say would be bad practice.
So i have tried to minimize this as much as possible and here is my leasy amount of code to reproduce it:
import SwiftUI
enum Loadable<T> {
case loaded(T)
case notRequested
}
struct SuperBinder {
let loadable: Binding<Loadable<ContentView.ViewModel>>
let toggler: Binding<Bool>
}
struct Holder {
var loadable: Loadable<ContentView.ViewModel> = .notRequested
var toggler: Bool = false
func generateContent(superBinder: SuperBinder) {
superBinder.loadable.wrappedValue = .loaded(.init(toggleBinder: superBinder.toggler))
}
}
struct ContentView: View {
#State var holder = Holder()
var superBinder: SuperBinder {
.init(loadable: $holder.loadable, toggler: $holder.toggler)
}
var body: some View {
switch holder.loadable {
case .notRequested:
Text("")
.onAppear(perform: {
holder.generateContent(superBinder: superBinder)
})
case .loaded(let viewModel):
Toggle("testA", isOn: viewModel.toggleBinder) // Works but doesn't update UI
Toggle("testB", isOn: $holder.toggler) // Works completly
// Pressing TestA will even toggle TestB
// TestB can be turned of directly but has to be pressed twice to turn on. Or something?
}
}
struct ViewModel {
let toggleBinder: Binding<Bool>
}
}
anyone else stumbled across the same problem? Or do i not understand how binders work?
Related
I have a custom ViewModifier which simply returns the same content attached with a onReceive modifier, the onReceive is not triggered, here is a sample code that you can copy, paste and run in XCode:
import SwiftUI
import Combine
class MyViewModel: ObservableObject {
#Published var myProperty: Bool = false
}
struct ContentView: View {
#ObservedObject var viewModel: MyViewModel
var body: some View {
Text("Hello, World!")
.modifier(MyOnReceive(viewModel: viewModel))
.onTapGesture {
self.viewModel.myProperty = true
}
}
}
struct MyOnReceive: ViewModifier {
#ObservedObject var viewModel: MyViewModel
func body(content: Content) -> some View {
content
.onReceive(viewModel.$myProperty) { theValue in
print("The Value is \(theValue)") // <--- this is not executed
}
}
}
is SwiftUI designed to disallow onReceive to execute inside a ViewModifier or is it a bug ? I have a view in my real life project that gets bigger with some business logic put inside onReceive, so I need to clean that view by separating it from onReceive.
ok, this works for me:
func body(content: Content) -> some View {
content
.onAppear() // <--- this makes it work
.onReceive(viewModel.$myProperty) { theValue in
print("-----> The Value is \(theValue)") // <--- this will be executed
}
}
ObservableObject and #Published are part of the Combine framework if you aren't using Combine then you shouldn't be using a class for your view data. Instead, you should be using SwiftUI as designed and avoid heavy objects and either put the data in the efficient View data struct or make a custom struct as follows:
import SwiftUI
struct MyConfig {
var myProperty: Bool = false
mutating func myMethod() {
myProperty = !myProperty
}
}
struct ContentView: View {
#State var config = MyConfig()
var body: some View {
Text("Hello, World!")
.onTapGesture {
config.myMethod()
}
}
}
Old answer:
Try onChange instead
https://developer.apple.com/documentation/swiftui/scrollview/onchange(of:perform:)
.onChange(of: viewModel.myProperty) { newValue in
print("newValue \(newValue)")
}
But please don't use the View Model object pattern in SwiftUI, try to force yourself to use value types like structs for all your data as SwiftUI was designed. Property wrappers like #State will give you the reference semantics you are used to with objects.
I know that this may be tagged as a repeat question, but the ones that exist have not helped me implement delegates in Swift with SwiftUI. My issue can be see in the code below. I have a View and VM, TodaysVM. In here, I do a network request, that downloads movie data from an API, and saves the data to CoreData. In order to show graphs, the HabitVM needs to fetch the data from CoreData, and then do some long processing on it.
For this, I was looking into a way for one class to send a message notification to another class. So I stumbled onto using delegates (not sure if this is correct?). But essentially the idea would be that the TodayVM refreshes its data (which the user can do a pull to refresh on this page), it also sends a message to HabitsVM to start processing (so the data can be done by the time the user navigates to this page. I am not sure if there is a better way to do this, and I was trying to stay away from a singleton approach, passing one class into class, or creating a new instance of one of the classes.
In the code below, I have the delegates set up but 'print("Data update required!")' is never executed. From my research I understand this to be because I never set a value to the delegate? So the delegate value is nil, and therefore the
delegate?.requireUpdate(status: true)
Is never actually executed. Which makes sense, since delegate is an optional, but never assigned a value. So I was curious how I would assign it a value in SwiftUI? And if using delegates is even the best method to have to separate classes send updateRequired status updates.
import SwiftUI
import Foundation
// protocol
protocol MovieNotifierDelagate: AnyObject {
func requireUpdate(status: Bool)
}
struct HabitView {
#StateObject var habitVM = HabitsVM()
var body: some View {
Text("Hello")
}
}
struct TodaysView {
#StateObject var todayVM = TodayVM()
var body: some View {
Text("Hi!")
}
}
class HabitsVM: ObservableObject, MovieNotifierDelagate {
init() {
}
func requireUpdate(status: Bool) {
print("Data update required!")
reprocessData()
}
func reprocessData() {
print("Processing")
}
}
class TodayVM: ObservableObject {
weak var delegate: MovieNotifierDelagate?
init() {
//Does some network call and downloads data, and saves to CoreData
sendUpdate(status: true)
}
func sendUpdate(status: Bool) {
guard status else {
print ("No update")
return
}
delegate?.requireUpdate(status: true)
}
}
// Controlling Page
struct HomeView: View {
//Default the user to the today view
#State private var selection = 1
var body: some View {
TabView(selection: $selection) {
HabitView()
.tag(0)
.tabItem {
VStack {
Image(systemName: "books.vertical")
Text("Habits")
}
}
TodaysView()
.tag(1)
.tabItem {
VStack {
Image(systemName: "backward.end.alt")
Text("Rewind")
}
}
}
}
}
I am using SwiftUI/Swift for a week now, and I love it. Now I have a problem. I want to call a Function from my View, but I get this Error
Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols
This is the Code:
struct testView: View {
var body: some View {
VStack {
Text("TextBox")
Text("SecondTextBox")
self.testFunction()
}
}
func testFunction() {
print("This is a Text.")
}
}
I don't get it. In other languages its much simpler and could work that way. Can anybody help me please? Swift is pretty new to me :D
Meanwhile here are the places (not all) where/how you can call a function
init() {
self.testFunction() // non always good, but can
}
var body: some View {
self.testFunction() // 1)
return VStack {
Text("TextBox")
.onTapGesture {
self.testFunction() // 2)
}
Text("SecondTextBox")
}
.onAppear {
self.testFunction() // 3)
}
.onDisappear {
self.testFunction() // 4)
}
}
... and so on
An additional method:
Testing with Swift 5.8, you can also stick in a let _ = self.testFunction().
eg
(this is extra-contrived so that it's possible to see the effect in Preview, because print() doesn't happen in Preview, at least for me)
import SwiftUI
class MyClass {
var counter = 0
}
struct ContentView: View {
var myClass: MyClass
var body: some View {
VStack {
Text("TextBox counter = \(myClass.counter)")
// v--------------------------------------------
//
let _ = self.testFunction() // compiles happily
// self.testFunction() // does not compile
//
// ^--------------------------------------------
Text("TextBox counter = \(myClass.counter)")
}
}
func testFunction() {
print("This is a Test.")
myClass.counter += 1
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(myClass: MyClass())
}
}
Using Swift 5.3 and Xcode 12.4
I use a little extension to debug inside the Views (VStack in the example), e.g. to inspect a geometryReader.size. It could be used to call any function in the View as follows:
NOTE: For debugging purposes only. I don't recommend including code like this in any production code
VStack {
Text.console("Hello, World")
Text("TEXT"
}
extension Text {
static func console<T>(_ value: T) -> EmptyView {
print("\(value)")
return EmptyView()
}
}
This will print "Hello, World" to the console.....
There is a reason, when writing in swift UI (iOS Apps), that there is a View protocol that needs to be followed. Calling functions from inside the structure is not compliant with the protocol.
The best thing you can do is the .on(insertTimeORAction here).
Read more about it here
I want to programmatically be able to navigate to a link within a List of NavigationLinks when the view appears (building deep linking from push notification). I have a string -> Bool dictionary which is bound to a custom Binding<Bool> inside my view. When the view appears, I set the bool property, navigation happens, however, it immediately pops back. I followed the answer in SwiftUI NavigationLink immediately navigates back and made sure that each item in the List has a unique identifier, but the issue still persists.
Two questions:
Is my binding logic here correct?
How come the view pops back immediately?
import SwiftUI
class ContentViewModel: ObservableObject {
#Published var isLinkActive:[String: Bool] = [:]
}
struct ContentViewTwo: View {
#ObservedObject var contentViewModel = ContentViewModel()
#State var data = ["1", "2", "3"]
#State var shouldPushPage3: Bool = true
var page3: some View {
Text("Page 3")
.onAppear() {
print("Page 3 Appeared!")
}
}
func binding(chatId: String) -> Binding<Bool> {
return .init(get: { () -> Bool in
return self.contentViewModel.isLinkActive[chatId, default: false]
}) { (value) in
self.contentViewModel.isLinkActive[chatId] = value
}
}
var body: some View {
return
List(data, id: \.self) { data in
NavigationLink(destination: self.page3, isActive: self.binding(chatId: data)) {
Text("Page 3 Link with Data: \(data)")
}.onAppear() {
print("link appeared")
}
}.onAppear() {
print ("ContentViewTwo Appeared")
if (self.shouldPushPage3) {
self.shouldPushPage3 = false
self.contentViewModel.isLinkActive["3"] = true
print("Activating link to page 3")
}
}
}
}
struct ContentView: View {
var body: some View {
return NavigationView() {
VStack {
Text("Page 1")
NavigationLink(destination: ContentViewTwo()) {
Text("Page 2 Link")
}
}
}
}
}
The error is due to the lifecycle of the ViewModel, and is a limitation with SwiftUI NavigationLink itself at the moment, will have to wait to see if Apple updates the pending issues in the next release.
Update for SwiftUI 2.0:
Change:
#ObservedObject var contentViewModel = ContentViewModel()
to:
#StateObject var contentViewModel = ContentViewModel()
#StateObject means that changes in the state of the view model do not trigger a redraw of the whole body.
You also need to store the shouldPushPage3 variable outside the View as the view will get recreated every time you pop back to the root View.
enum DeepLinking {
static var shouldPushPage3 = true
}
And reference it as follows:
if (DeepLinking.shouldPushPage3) {
DeepLinking.shouldPushPage3 = false
self.contentViewModel.isLinkActive["3"] = true
print("Activating link to page 3")
}
The bug got fixed with the latest SwiftUI release. But to use this code at the moment, you will need to use the beta version of Xcode and iOS 14 - it will be live in a month or so with the next GM Xcode release.
I was coming up against this problem, with a standard (not using 'isActive') NavigationLink - for me the problem turned out to be the use of the view modifiers: .onAppear{code} and .onDisappear{code} in the destination view. I think it was causing a re-draw loop or something which caused the view to pop back to my list view (after approx 1 second).
I solved it by moving the modifiers onto a part of the destination view that's not affected by the code in those modifiers.
So I'm very familiar with UIKit but just recently started working with SwiftUI. I have this very basic view coming from a NavigationLink and I can't figure out why this code does not work. I can't update that #State var`, I tried everything you can't think of. Am I doing something wrong here? Thanks!
struct Test: View {
#State private var settingsEnabled: Bool = false {
didSet {
print(oldValue)
print("DID SET")
print(settingsEnabled)
}
}
var body: some View {
Button(action: {
self.settingsEnabled.toggle()
}) {
Text(String("\(self.settingsEnabled)"))
}
}
}
You seem to be missing the View conformance.
struct Test: View {
...
}
Update: The seems to have occurred because you were using the body of the Test struct directly instead of the Test struct that conforms to View protocol, which in-turn contains the settingsEnabled #State variable.