swiftswiftuicombine

Can and should you observe changes to an ObservedObject from regular Swift code


I am a bit lost in Combine at the moment. I have an ObservableObject. This is mostly because this model is used in SwiftUI and having properties @Published means swiftUI can update with them. However now I also have swift code that would like to listen to changes.

class TheModelClass: ObservableObject {

However this code does not compile... And I am a bit surprised?

modelChangeSink = self.model.objectWillChange.sink {
   let newValue: PageManagerObservableModel = $0 // This line does not compile
}

With the compile time error being Cannot convert value of type 'ObservableObjectPublisher.Output' to specified type 'PageManagerObservableModel' which I cant 100% interpret. I am ideally hoping to do things like

if (self.model.property != newValue.property) {
  // On property change
}

Should I be using something other than ObservableObject to do this? I cant find a lot of examples of people using ObservableObject outside of SwiftUI so perhaps I should be using something different?


Solution

  • The ObservableObjectPublisher has an associated type called Output and that is the type of the values that it publishes. Inside of your model change sink, that is all the information that the compiler has. That type must trace back to a concrete type, but the compiler doesn't know what that concrete type is. You're telling it to put whatever value that may be into a variable of type PageManagerObservableModel, but the compiler doesn't have enough information to say that ObservableObjectPublisher.Output and PageManagerObservableModel are the same type, so it gives an error.

    In all liklihood, TheModelClass.objectWillChange has an Output type of Void (it was in the small sample I tried). This basically means it will invoke subscribers, but not pass them a value.

    In normal usage when you want to use ObservableObject you are interested in specific properties of the object so you'd do something like:

    import UIKit
    
    class WatchMe: ObservableObject {
        @Published var myValue: Int;
        init() {
            myValue = 10
        }
    }
    
    let watchable = WatchMe()
    let subscription = watchable.$myValue.sink { newValue in
        print(newValue)
    }
    
    watchable.myValue = 20
    watchable.myValue = 30
    

    The value I'm actually interested in knowing about is myValue. Certainly you could use code like this to communicate between models. You should be aware that the observer is invoked during the willSet portion of the property being changed. So at the time your subscription is invoked, you get the new value, before it's actually assigned to the ObservedObject.

    Having said that, in my opinion, and as you seem to have noticed, ObservableObject and the newer variant @Observable are a publish/subscribe that shines in the context of SwiftUI. But I've had limited success using them outside of that context.

    I usually find that one of the other Observer Pattern implementations in the system seem to fit the models I create better. iOS has lots of implementations of the Observer Pattern to choose from. Off the top of my head:

    You could also go with Swift Concurrency and use an AsyncSequence to create a stream of changed values over time.