Subclass of an ObservableObject doesn't cause a render pass in SwiftUI when changing @Published properties defined in the subclass

I've spent quite some time narrowing down this problem, and have produced a minimal Playground example here:

import Combine
import SwiftUI
import PlaygroundSupport

class BaseViewModel: ObservableObject {
    @Published var title: String

    init(title: String) {
        self.title = title
    }
}

final class ViewModel: BaseViewModel {
    @Published var counter: Int = 0

    // Uncommenting this init method changes nothing.
//    override init(title: String) {
//        super.init(title: title)
//    }

    // But uncommenting *this* one stops the bug happening, just by accessing `objectWillChange`?!
//    override init(title: String) {
//        super.init(title: title)
//        objectWillChange // ?!?!??!
//    }
}

struct OuterView: View {
    @ObservedObject var model: ViewModel

    var body: some View {
        VStack(spacing: 16) {
            Button(action: {
                // Incrementing the counter doesn't cause a render of the view.
                self.model.counter += 1
            }, label: {
                Text("Counter: \(model.counter)")
            })

            Button(action: {
                // Changing the title (which is on the base class) works correctly,
                // and shows that the counter clicks are working, but not rendering.
                self.model.title += " ✅"
            }, label: {
                Text("Title: \(model.title)")
            })
        }
    }
}

let model: ViewModel = .init(title: "Example")
PlaygroundPage.current.liveView = UIHostingController(rootView: OuterView(model: model))

In short, modifying a published property that is defined in a subclass doesn't cause a render pass to happen in SwiftUI. If you attach a subscriber to model.$counter then you can see it is emitting events correctly, and the same is true of model.objectWillChange. For some reason, SwiftUI doesn't receive those events, or thinks the model object hasn't changed? Whatever the reason, the render pass doesn't happen.

You can see the bug in action here:
Screen Recording 2020-11-09 at 14.07.04.2020-11-09 14_10_44

The title property is defined on the base class and works as expected. The counter property is defined on the subclass and doesn't cause the view to render. What I find exceptionally weird is that if I access objectWillChange in any way in the subclass's init method, the bug goes away?!

Similar issues are discussed in this question:

I ran into the same issue as you did with a base class viewmodel. For me using objectWillChange didn't solve the issue but when i made the child class implement the protocol ObservableObject it started working. Maybe swift checks if the class implements the protocol and doesn't check further up in the inheritance chain.

Is there any update on this issue? I have a case where I have a view that serves as a detail for two separate entities which have this information in common so to reduce duplication I made a subclass which has this component and both separate entities inherit from it.

So the problem is, the @Published properties in the subclasses do not send their objectWillChange message. I can't put the OservableObject compliance on the subclasses because it's needed on the base class (since the shared view only references the base class the base class must be compliant with the ObservableObject protocol). Trying to add it on the subclasses fails the compile with the "redundant conformance to protocol" error.

This thread is all about Apple-private frameworks (SwiftUI and Combine). Have you files a bug report at https://feedback.apple.org? In any case, you probably won't know until a MacOS/iOS/... upgrade is announced. You might ask about this issue at the Apple Developer forums.

Fair enough. I have done so now.

EDIT: But the link you showed seems to be dead. I used https://feedbackassistant.apple.com

Any updates on this?

How is conformance to ObservableObject synthesised? Isn't this bit a part of the compiler?.

Nope. Implemented in protocol extension inside the Combine, uses some sort of side table under the hood. So definitely off-topic.

This is fixed in iOS 14.5 Beta2? https://twitter.com/krzyzanowskim/status/1361768063587151872

Terms of Service

Privacy Policy

Cookie Policy