Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to listen to an ObservableObject

Ok, so SwiftUI and ObservableObject, on iOS 13. I have Model that implements ObservableObject:

class Model: ObservableObject {
    @Published public var toggle: Bool = false

    init() {
        NSLog("Model init")
        objectWillChange.sink { void in
            NSLog("1 toggle \(self.toggle)")
        }
        $toggle.sink { v in
            NSLog("2 toggle \(self.toggle) -> \(v)")
        }
    }
}

and a Button that toggles toggle:

struct ContentView: View {
    @ObservedObject var model: Model
    
    var body: some View {
        Button(action: {
            self.model.toggle.toggle()
        }, label: {Text(model.toggle ? "on" : "off")})
    }
}

Now, this works. You hit the button and it toggles between "on" and "off". (Before making toggle @Published, it did not.) However, the logging does not work as expected. I get two logs immediately on startup: "Model init" and "2 toggle false -> false". Tapping the button, though apparently changing the value of toggle, does not cause either of the closures to execute.

When the view mutates your model, I'd expect there to be a way to be informed of the change, in case you need to e.g. update calculated values or sync to disk or something. Perhaps sink is the wrong method?

How can an ObservableObject with @Published fields be notified when its fields are updated?

like image 677
Erhannis Avatar asked Jan 22 '20 03:01

Erhannis


People also ask

How do you conform to ObservableObject?

To conform to ObservableObject, simply add it to the class definition. ObservableObject provides a default implementation for objectWillChange using Combine/ObservableObjectPublisher. To trigger objectWillChange events when your data changes, annotate your properties with the @Published property wrapper.

How do you use observed objects?

When using observed objects there are three key things we need to work with: the ObservableObject protocol is used with some sort of class that can store data, the @ObservedObject property wrapper is used inside a view to store an observable object instance, and the @Published property wrapper is added to any ...

What is ObservedObject?

A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.

How do I make an observable object in Swiftui?

Open Xcode and either click Create a new Xcode project in Xcode's startup window, or choose File > New > Project. In the template selector, select iOS as the platform, select the Single View App template, and then click Next.


2 Answers

The latest documentation for the the return value on the sink function:

/// - Returns: A cancellable instance; used when you end assignment of the received value. Deallocation of the result will tear down the subscription stream.

Essentially this means that sink makes a Subscriber but doesn't retain it. As soon as your init is done the subscribers are torn down and removed from memory. You need to keep them around by creating a strong reference like this:

class Model: ObservableObject {
    @Published public var toggle: Bool = false

    var changeSink: AnyCancellable?
    var toggleSink: AnyCancellable?

    init() {
        NSLog("Model init")
        changeSink = objectWillChange.sink { void in
            NSLog("1 toggle \(self.toggle)")
        }
        toggleSink = $toggle.sink { v in
            NSLog("2 toggle \(self.toggle) -> \(v)")
        }
    }
}

I haven't used much Combine but an alternative I've seen often that you might consider is just adding a didSet to your attribute like so:

    public var toggle: Bool = false {
        didSet {
            print("1 toggle \(self.toggle)")
        }
    }
like image 162
Joe Avatar answered Sep 28 '22 13:09

Joe


Your ObservableObject Class Model is done correctly, But:

1. ObjectWillChange should be of type ObservableObjectPublisher().

That creates an objectWillChange property as an instance of ObservableObjetPublisher. This comes from the Combine framework, which is why you need to add import Combine to make your code compile. The job of an observable object publisher is simple: whenever we want to tell the world that our object has changed, we ask the publisher to do it for us.

2. the property you need to observe (Toggle) should be implemented this way:

 var toggle = "" {
        willSet {
            objectWillChange.send()
        }
    }

Second, we have a willSet property observer attached to the Toggle property of Model so that we can run code whenever that value changes. In our your example code, we call objectWillChange.send() whenever toggle changes, which is what tells the objectWillChange publisher to put out the news that our data has changed so that any subscribed views can refresh.

3. make sure your class Model conforms to ObservableObject, and the its instance is marked with @ObservedObject

As Your Model class conforms to ObservableObject, you can use it just like any other @ObservedObject property. So, we might use it like this to watch the toggle, like this:

struct ContentView: View {
    @ObservedObject var model: Model

    var body: some View {
        Button(action: {
            self.model.toggle.toggle()
        }, label: {Text($model.toggle ? "on" : "off")})
    }
}

Hope this helps, reference: https://www.hackingwithswift.com/quick-start/swiftui/how-to-send-state-updates-manually-using-objectwillchange

like image 27
Sanad Barjawi Avatar answered Sep 28 '22 11:09

Sanad Barjawi