I began messing with combine in one of my personal projects and I am trying to use combine to allow my controllers and views to subscribe to different values in my app state.
I created an app state that has @Published properties, and that’s great, I can subscribe to changes on those properties, but in the case of a controller wanting to subscribe to the entire app state, it seems as though the option is only ObservableObject My confusion arises in the fact that this protocol exposes objectWillChange and not something like objectDidChange. Which means when I add a sink to it, I’m only ever getting the old value of state instead of the most recent. Am I missing something? Is this there a better way that I’m not aware of?
You're not really missing anything here - it's a very focused API endpoint (that I believe was meant to support some of the internals/optimizations of SwiftUI - but I'm asserting that with no detailed knowledge).
the objectWillChange publisher's Output type is type-aliased to Void, so while you'll get a triggered notification, it's also not actually passing any changed values - just the notification that the referenced object was, in fact, changed (or is about to change) in some fashion - so that your code can open up that object and inspect it for what it is after the change.
I'm rewriting one of my apps in SwiftUI, and I'm just wondering why the change from didChange to objectWillChange took place.
I find it odd that we can only be notified when something will change and that we have no way to react to the new value.
Currently, I am using @topchul's solution with the DispatchQueue (though I need to do asyncAfter and wait a few milliseconds before I can access a new value), but it just doesn't feel like it is the right solution to this problem. It definitely works, though.
I'm using SwiftUI, and my intention is to know when a certain ObservableObject has a non-nil error I can inspect. Currently, it looks like this:
.onReceive(schedule.objectWillChange) { _ in
// We can also use schedule.objectWillChange to get the publisher.
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
if self.schedule.provider.error != nil {
self.showingError = true
}
}
}
I believe that when an observed object changes, SwiftUI internally simply invalidates the view hierarchy, rerendering and diffing it at a later point, possibly on the next run loop. This would be an important optimization to prevent superfluous rendering passes when an object posts multiple of these notifications in quick succession: Imagine changing two or more of an observed object's properties in the same block of code — this should only result in one rendering pass. I'd imagine this is implemented similar to setNeedsDisplay() or setNeedsLayout().
As to why they used 'will change' over 'did change', I am not sure. In an early beta, it actually was called objectDidChange! But I guess that with objectWillChange it is harder to misuse the API: you are forced to debounce a call to read the new value.
But I, too, have no knowledge of the SwiftUI or Combine internals — so take that with a grain of salt.
This is a good idea. Unfortunately it doesn't work for me because I need showingError to be a state variable so I can use it with the .sheet modifier. I tried something similar to that, but you cannot use property wrappers with computed properties
I observe the same thing when using objectWillChange from a ViewController (using UIKit).
Is there a recommended approach other than pushing to the main thread using DispatchQueue.main.async or receive(on: RunLoop.main) to get the correct value (after the set - that is) or is that what we're supposed to do?
Just want to make sure I'm teaching good practices.
@Published is effectively very SwiftUI focussed, adds a willSet property observer to properties so that any changes are automatically sent out to observers.
An alternative to achieve such behavior can be performed as shown below. However, your code should handle the different cases as the object emits multiple notifications on willSet and didSet
class Foo: ObservableObject {
var someProperty: String {
didSet {
objectWillChange.send()
}
}
}