Question about valid uses of `ObservableObject`'s synthesized `objectWillChange`

I'm seeing some strange behavior around the synthesized objectWillChange when trying to access it manually, and I'm curious if I'm seeing a bug or if there's a good explanation for why it works this way.

The following ViewModel compiles (i.e. objectWillChange is synthesized), but if I bind it to a view using @ObservedObject the view is not updated as the names cycle:

class ViewModel: ObservableObject {
    var name: String = "John" {
        willSet {
            objectWillChange.send()
        }
    }

    private let names = ["John", "Fred", "Jake"]

    init() {
        cycleNames()
    }

    func cycleNames(startAt index: Int = 0) {
        name = names[index]
        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self] in
            guard let self = self else { return }
            let next = index + 1
            if next == self.names.endIndex {
                self.cycleNames(startAt: 0)
            } else {
                self.cycleNames(startAt: next)
            }
        }
    }
}

If I manually add let objectWillChange = ObservableObjectPublisher() in ViewModel, the view updates properly as the names cycle.

To make matters more confusing, if I don't include the manual objectWillChange but do include a separate published property e.g. @Published var age: Int = 1, then the view updates properly as the names cycle.

So it looks like manually calling send() on a synthesized objectWillChange doesn't work unless there is at least one @Published property. Is this intended behavior or a bug?

The synthesized objectWillChange creates an ObservableObjectPublisher once and installs it into all the @Published properties using runtime introspection. When you access objectWillChange, it will take out the installed instance and return it to you.

Since there's no @Published properties in your class, there's no storage objectWillChange could install the publisher into, so it just creates and returns a new instance every time you access objectWillChange.

So in your case you should either make your name property @Published, or you provide a custom implementation of the objectWillChange property (trivially, by adding a stored constant to your ViewModel class)

If you're curious how it's implemented, you can refer to my attempt at replicating this behavior.

1 Like

Seeing your implementation is really helpful! I had assumed that objectWillChange was statically synthesized with some kind of compiler magic.

So if I understand correctly, Published has space for the objectWillChange reference and the same ObservableObjectPublisher is put into every published property when objectWillChange is accessed?

Exactly.

1 Like

This feels very unintuitive, and borderline unexpected behaviour.

1 Like

It was definitely mystifying to me that objectWillUpdate existed but my view wasn’t updating, though it makes sense once you understand the implementation. I wonder if making this an assertion failure in debug builds with a helpful message could help prevent confusion.

This was discussed previously. @Philippe_Hausler asked me to file an FB on it, which I did: Is there possible to support inheriting from a generic type: class classA<T: classB>: T?. There has not been an update on that FB since I filed it.

It's still very likely not what you want, to have new objectWillChange every time. It's still that the synthesis is not doing a good job deciding a default behaviour.

@anandabits Funny that I independently filed mine last month as well (FB7321036). With no update either.

I agree, but I’m not sure there’s a possible default behavior that makes sense, so a crash seems appropriate to me.

Terms of Service

Privacy Policy

Cookie Policy