How does SwiftUI update if objectWillChange fires *before* change

I'm wondering how SwiftUI updates work in connection with ObservableObjects. If a SwiftUI View depends on an `ObservableObject`, the object's `objectWillChange` publisher fires, and SwiftUI learns about the change, before the change happens. At this point, SwiftUI can't re-render a View, right? Because the new properties aren't there yet. So what does SwiftUI do? Does it schedule the change for later? That doesn't make sense either - how can it know when the object will be ready to be used in a new rendering of the UI?


~ Rob

Replies

Generally speaking, objectWillChange is called immediately before the new value is written. Further, the expectation within Combine is that the old value will only be available until the moment the objectWillChange call completes, and not afterwards; the publisher is expected to enforce this. This is one of the reasons for the existence of the @Published property wrapper, and the change from the previous guidance of just using your own 'objectWillChange' subject—the property wrapper can enforce certain behaviors, for instance locking access to its wrapped value from the point it signals the will-change event to the time the item has actually changed. For instance, a simplistic implementation might look something like this:


@propertyWrapper
struct Published {
    let publisher: PassthroughSubject<void, never=""> // this is a reference type
    
    init(publisher: PassthroughSubject, wrappedValue: Value<void, never="">) {
        self.publisher = publisher)
        self._wrappedValue = wrappedValue
    }

    var lock = SomeLock()
    private var _wrappedValue: Value
    private var _oldValue: Value!
    var wrappedValue: Value {
        get { 
            if lock.tryLock() {
                return _wrappedValue
            } else {
                // we're updating, and the contract is that _oldValue *must* be valid while locked.
                return _oldValue
            }
        }
        set {
            _oldValue = _wrappedValue
            lock.whileLocked {
                publisher.send()
                _wrappedValue = newValue
            }
        }
    }
}


The actual implementation will be significantly more gnarly than this, and would really need to involve some sort of multi-state condition lock (such as NSConditionLock) that would allow the getter and setter to more efficiently synchronize themselves: so the getter can read when in not-updating state and in will-change state, but not in actually-changing state, etc. (read up on condition locks and multi-threaded queue implementations to find some basic examples).

> ... old value will only be available until the moment the objectWillChange call completes, and not afterwards; the publisher is expected to enforce this. This is one of the reasons for the existence of the @Published property wrapper, and the change from the previous guidance of just using your own 'objectWillChange' subject


I'm curious if you found this in the docs somewhere, or ??. I can't find it in the docs for `ObservableObject` protocol or it's method `objectWillChange`. Or on the `Published` struct.


In some situations I would prefer to use my own `objectWillChange` publisher, intead of @Published on properties. I'm now worried that going that way, without extra locking code like your example, could lead to race condition bugs where SwiftUI sometimes fails to properly update Views.

I can sometimes reproduce this very issue in my app:

  • willSet is executed
  • then view body is computed with old value of property
  • then didSet and property changes, but view already shows previous value

If anyone have seen something similar and know possible solutions, please let me know