Combine ObservableObject in UIKit

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?

3 Likes

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.

Hello connor.

objectWillChange would publish before set newValue. and Ouput = Void
so, you can not access newValue.

But, you can access newValue next dispatching after change operation.

someObservableObject.objectWillChange
    .makeConnectable()
     .autoconnect()
      .sink { [weak self] in
          print(self?.someObservableObject)
          DispatchQueue.main.async { [weak self] in
              print(self?.someObservableObject)
          }
      }

this code will print oldValue and newValue.

1 Like

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
                }
            }
        }
2 Likes

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.

1 Like

I remember reading this tweet.

4 Likes

Looks like you could make showingError a function of scheduleProvider.

var showingError: Bool {
  schedule.provider.error != nil
}

and use that instead of stored property.

Thanks. That's insightful, and it does make sense.

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 :disappointed:

Could you provide more code as to what you’re trying to do (And may as well create a separate thread)?

Sure. Right now it's low priority since it works with the dispatch, but I will create a thread later and tag you there so you can take a look at it.

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.

Thanks,

Daniel

@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()
        }
    }
}