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:
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: