In an effort to maintain the "single source of truth," I have a data model that contains a User object (class, not struct), and a UserManager object that performs user-related actions at the behest of SwiftUI views. All the members of User are basic types and are published.
The UserManager holds a reference to this User object that resides in the central data model. This User object might be changed by a background process, as the result of polling a remote database (for example). However, changes to embedded objects do not trigger notifications that their parent has changed, and thus wouldn't be reflected by a UI looking at the UserManager.
Therefore I wanted the UserManager to subscribe to changes to its User object, and then alert the UI to refresh itself.
So I set up the following, being sure to store a reference to the cancellable pipeline.
class User : Codable, Equatable, ObservableObject
{
@Published var ID: String = ""
@Published var pw: String = ""
@Published var username: String = ""
@Published var firstName: String = ""
@Published var lastName: String = ""
...
}
class CentralModel : ObservableObject
{
@Published var user: User
init()
{
user = User()
}
...
}
class UserManager : ObservableObject
{
var model: CentralModel
private var pubPipelines: Set<AnyCancellable> = []
init(withModel: CentralModel)
{
self.model = withModel
model.user.objectWillChange.sink(receiveValue: { newUser in
print("The new user is \(newUser)") })
.store(in: &pubPipelines)
startValidationCheckTimer()
}
func startValidationCheckTimer()
{
self.validationTimer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(self.timedValidationCheck), userInfo: nil, repeats: true)
}
@objc func timedValidationCheck()
{
Task { @MainActor in
user.firstName = UUID().uuidString
print("Set user's first name to \(user.firstName)")
}
}
...
}
In the app I create a timer that randomly changes user.firstName. The objectWillChange sink is never called. Because I'm subscribing directly to the embedded User's publisher, I wouldn't expect that the UserManager's pointer to it needs to change to trigger a publication.
So the first question is why the sink's not receiving anything. I even tried setting up a dummy User in CentralModel and subscribing to that one, but swapping it out entirely by setting it to User() (thus changing CentralModel's pointer to it). Still nothing.
Beyond that, I'm not a huge fan of this type of design, but I don't see how else to have a "single source of truth."
I took your code and put it into a playground. Once I had fixed all the errors the compiler generated it works fine - the
sinkonobjectWillChangeis called normally.One thing I noticed along the way was the you were expecting the
objectWillChangepublisher to publish a value.objectWillChangeis basicallyAnyPublisher<Void, Never>meaning it will not publish any value (if you put a.print()operator in the pipeline ahead of thesinkyou will see it publishes the unit value or()) soreceiveValuedoesn't provide you with theuserthe way your code suggests it should.The next thing that may trip you up is
objectWillChangeis sent during the property'swillSethandler, and the value of the property will not be set to its new value until AFTER theobjectWillChangepublisher has done its thing. So you can't retrieve the newfirstNamefrom the user because the property hasn't changed yet. This playground will print the previous value of thefirstNamefield, but not the new one.Finally
@Published,@ObservableObjectand related mechanisms are primarily intended for interactions between a model and aSwiftUIview that is displaying values from that model.Based on the challenges of that system, it is my opinion that it was ever intended to be used for generic notification of model changes throughout the system. There are any of a number of other Publish/Subscribe systems you could use. In comments you mentioned the
NotificationCenterand that certainly is one choice.Combineis another. TheDelegatepattern is a third option. Simple closures (callbacks) is also a choice. As others noted the new macro-based@Observableis a better step toward a generic mechanism, but as mentioned in comments - it's new :-(.At any rate, here is a playground of your code showing something close to what you are trying to do given the caveat's above: