swiftswiftuiobservableobjectproperty-wrapper-published

Changing a `Published` property of an `ObservableObject` to a computed var


I have an ObservableObject with @Published properties:

class SettingsViewState: ObservableObject {
    @Published var viewData: SettingsViewData = .init()
    …

I would like to change viewData to a computed var based on other sources of truth rather than allowing it to be directly modified. However, I still want views looking at viewData to automatically update as it changes. They should update when properties it is computed from change.

I'm really not very certain about how @Published actually works though. I think it has its willSet perform objectWillChange.send() on the enclosing ObservableObject before a change occurs, but I'm not sure!

If my suspicion is correct, it seems like I could manually call objectWillChange.send() on the enclosing object if anything viewData depends on will change.

Alternatively, if properties viewData is computed from are themself @Published, when I change one, presumably an equivalent objectWillChange.send() will occur automatically, and I won't need to do anything special? This should work even if these properties are private and a watching view doesn't have access to them: it should still see the objectWillChange being emitted?

However, It's entirely possible I've got this horribly garbled or mostly backwards! Eg, perhaps the @Published properties have their own independent change publisher, rather than simply making use of the enclosing ObservableObject's? Or both of them publish prior to a change?

Clarification will be gratefully received. Thank you!


Solution

  • I'm really not very certain about how @Published actually works though. I think it has its willSet perform objectWillChange.send() on the enclosing ObservableObject before a change occurs, but I'm not sure!

    You are correct, that is how @Published and ObservableObject work together, however, these are 2 independent things.

    @Published is a property wrapper, which adds a Publisher to the wrapped property, which emits the new value from the willSet of the property. So the Published.Publisher emits the value that is about to be set before it is actually set.

    ObservableObject is a protocol, which has an objectWillChange publisher, whose value is autosynthesised by the compiler. The synthesised implementation emits a value when any of the @Published property's publishers emits. So objectWillChange emits a value whenever an @Published property on the ObservableObject conformant type is about to change.

    If you store an ObservableObject conformant type on a SwiftUI View as @StateObject, @ObservedObject or @EnvironmentObject, the view updates (and hence recalculates its body) whenever the objectWillChange of the ObservableObject emits.

    If you have a property, which needs to be recalculated whenever other properties are updated and you want to update your view with these changes, you have several options to achieve that.

    1. Declare all properties as stored @Published properties and set up the dependant property to be updated whenever any of the properties it depends on are updated.

    This 100% guarantees that your view will always be updated with the correct values and you don't need to call objectWillChange.send() manually.

    This solution also works even if the properties that your "computed" property depends on are declared on other types, since your "computed" property is @Published so whenever it is updated, it will trigger a view update.

    However, you do need to set up the observation of your dependant properties to update the property that depends on them.

    @Published var height: Int
    @Published var width: Int
    
    @Published private(set) var size: Int
    
    init(height: Int, width: Int) {
      self.height = height
      self.width = width
      self.size = height * width
      $height.combineLatest($width).map { height, width in
        height * width
      }.assign(to: &$size)
    }
    
    1. If all all properties that your computed property depend on are @Published and are declared on the same object as your computed property, simply declaring the property that depends on them as computed should work, since the properties that you depend on will trigger a view update themselves whenever they are updated.

    This doesn't work though if your @Published properties are declared on another object, since that object won't trigger a view update.