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?
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.
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 ...
A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.
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.
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)")
}
}
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With