Swift
ObservableObject
Published
property wrapper
subclass

Published property wrapper not working on subclass of ObservableObject

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

@Published properties declared in a subclass of an ObservableObject do not trigger SwiftUI view updates because the objectWillChange publisher is synthesized only in the class that declares the ObservableObject conformance — the base class. The subclass's @Published properties silently fail to notify SwiftUI. The fix is to manually call objectWillChange.send() in the subclass, override the property with a willSet observer, or restructure the code to avoid inheritance (use composition or protocols instead).

The Problem

swift
1import SwiftUI
2import Combine
3
4// Base class conforms to ObservableObject
5class BaseViewModel: ObservableObject {
6    @Published var baseValue: String = "Base"
7}
8
9// Subclass adds its own @Published property
10class ChildViewModel: BaseViewModel {
11    @Published var childValue: String = "Child"  // This does NOT trigger view updates
12}
13
14struct ContentView: View {
15    @StateObject var viewModel = ChildViewModel()
16
17    var body: some View {
18        VStack {
19            Text(viewModel.baseValue)    // Updates correctly
20            Text(viewModel.childValue)   // Does NOT update when childValue changes
21            Button("Change Child") {
22                viewModel.childValue = "Updated"  // View does not refresh
23            }
24        }
25    }
26}

When you tap "Change Child", the view does not update even though childValue changed.

Why This Happens

Swift synthesizes the objectWillChange publisher (a PassthroughSubject<Void, Never>) in the class that declares ObservableObject conformance. The compiler generates willSet observers for @Published properties in that class to call objectWillChange.send(). Subclass @Published properties create their own internal Publisher, but they do not hook into the parent's objectWillChange. SwiftUI listens to objectWillChange — not the individual @Published publishers — so subclass changes are invisible.

Fix 1: Manual objectWillChange.send()

Call the publisher explicitly using willSet or a custom setter:

swift
1class ChildViewModel: BaseViewModel {
2    var childValue: String = "Child" {
3        willSet {
4            objectWillChange.send()
5        }
6    }
7}

Or using a computed property backed by a private stored property:

swift
1class ChildViewModel: BaseViewModel {
2    private var _childValue: String = "Child"
3
4    var childValue: String {
5        get { _childValue }
6        set {
7            objectWillChange.send()
8            _childValue = newValue
9        }
10    }
11}

Fix 2: Use Combine to Forward Changes

Subscribe to the subclass's published property and forward to objectWillChange:

swift
1class ChildViewModel: BaseViewModel {
2    @Published var childValue: String = "Child"
3
4    private var cancellables = Set<AnyCancellable>()
5
6    override init() {
7        super.init()
8
9        // Forward @Published changes to objectWillChange
10        $childValue
11            .sink { [weak self] _ in
12                self?.objectWillChange.send()
13            }
14            .store(in: &cancellables)
15    }
16}

This approach works but adds boilerplate for every @Published property in the subclass.

Fix 3: Avoid Inheritance — Use Composition

The cleanest solution is to avoid subclassing ObservableObject entirely:

swift
1class SharedState: ObservableObject {
2    @Published var baseValue: String = "Base"
3}
4
5class ChildViewModel: ObservableObject {
6    @Published var childValue: String = "Child"
7
8    let shared: SharedState
9
10    init(shared: SharedState) {
11        self.shared = shared
12    }
13}
14
15struct ContentView: View {
16    @StateObject var shared = SharedState()
17    @StateObject var viewModel: ChildViewModel
18
19    init() {
20        let shared = SharedState()
21        _shared = StateObject(wrappedValue: shared)
22        _viewModel = StateObject(wrappedValue: ChildViewModel(shared: shared))
23    }
24
25    var body: some View {
26        VStack {
27            Text(shared.baseValue)       // Updates correctly
28            Text(viewModel.childValue)   // Updates correctly
29        }
30    }
31}

Fix 4: Protocol-Based Approach

Use protocols instead of class inheritance:

swift
1protocol ViewModelProtocol: ObservableObject {
2    var baseValue: String { get set }
3}
4
5class ChildViewModel: ObservableObject, ViewModelProtocol {
6    @Published var baseValue: String = "Base"
7    @Published var childValue: String = "Child"  // Works because this class owns ObservableObject
8}

Since ChildViewModel directly conforms to ObservableObject, all its @Published properties work correctly.

Fix 5: @Observable Macro (iOS 17+)

The @Observable macro (Observation framework) fixes this issue entirely:

swift
1import Observation
2
3@Observable
4class BaseViewModel {
5    var baseValue: String = "Base"
6}
7
8@Observable
9class ChildViewModel: BaseViewModel {
10    var childValue: String = "Child"  // Works correctly with inheritance
11}
12
13struct ContentView: View {
14    var viewModel = ChildViewModel()
15
16    var body: some View {
17        VStack {
18            Text(viewModel.baseValue)    // Updates correctly
19            Text(viewModel.childValue)   // Updates correctly
20            Button("Change") {
21                viewModel.childValue = "Updated"  // View refreshes
22            }
23        }
24    }
25}

The @Observable macro tracks property access at the individual property level rather than relying on objectWillChange, so subclass properties work correctly.

Common Pitfalls

  • Assuming @Published works in all subclasses: @Published only triggers objectWillChange.send() in the class that declares ObservableObject conformance. Every subclass level needs its own mechanism to notify the publisher. This is a well-known limitation of the Combine-based observation system.
  • Using didSet instead of willSet: SwiftUI reads the new value during the objectWillChange notification. If you call objectWillChange.send() in didSet, the value has already changed but SwiftUI may have already captured the old state. Use willSet to send the notification before the change.
  • Forgetting [weak self] in Combine subscriptions: In Fix 2, omitting [weak self] in the sink closure creates a retain cycle between the view model and its cancellable set. Always use weak references in Combine subscribers.
  • Multiple inheritance levels compounding the issue: If you have Base -> Middle -> Child, each level needs its own forwarding. The problem gets worse with deeper hierarchies. Prefer composition or the @Observable macro for complex view model hierarchies.
  • Mixing @Observable and ObservableObject: You cannot use both @Observable and ObservableObject on the same class. If the base class uses ObservableObject, the subclass cannot use @Observable — you must migrate the entire hierarchy to one system or the other.

Summary

  • @Published in subclasses of ObservableObject does not trigger view updates because objectWillChange is synthesized only in the conforming base class
  • Quick fix: use willSet { objectWillChange.send() } on subclass properties instead of @Published
  • Better fix: use composition instead of inheritance for ObservableObject classes
  • Best fix (iOS 17+): migrate to the @Observable macro, which handles subclass properties correctly
  • Avoid deep ObservableObject hierarchies — they compound the forwarding problem at each level

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.