@Published property wrapper not working on subclass of ObservableObject
Asked Answered
G

7

42

I have a class conforming to the @ObservableObject protocol and created a subclass from it with it's own variable with the @Published property wrapper to manage state.

It seems that the @published property wrapper is ignored when using a subclass. Does anyone know if this is expected behaviour and if there is a workaround?

I'm running iOS 13 Beta 8 and xCode Beta 6.

Here is an example of what I'm seeing. When updating the TextField on MyTestObject the Text view is properly updated with the aString value. If I update the MyInheritedObjectTextField the anotherString value isn't updated in the Text view.

import SwiftUI

class MyTestObject: ObservableObject {
    @Published var aString: String = ""

}

class MyInheritedObject: MyTestObject {
    @Published var anotherString: String = ""
}

struct TestObserverWithSheet: View {
    @ObservedObject var myTestObject = MyInheritedObject()
    @ObservedObject var myInheritedObject = MyInheritedObject()

    var body: some View {
        NavigationView {
            VStack(alignment: .leading) {
                TextField("Update aString", text: self.$myTestObject.aString)
                Text("Value of aString is: \(self.myTestObject.aString)")

                TextField("Update anotherString", text: self.$myInheritedObject.anotherString)
                Text("Value of anotherString is: \(self.myInheritedObject.anotherString)")
            }
        }
    }
}
Griggs answered 22/8, 2019 at 19:25 Comment(4)
Not to be offending, but your posted code doesn't contain anything to suggest subclassing. Where is it?Nonpayment
I might be mixing terms but the class MyInheritedObject inherits from class MyTestObject?Griggs
I'm not sure if I understand you correctly, or perhaps I've not explained my issue properly? The code already creates two classes, MyTestObject: ObservableObject and class MyInheritedObject: MyTestObject. If you copy paste the exact code as I've described you can test and see for yourself that the value of self.myInheritedObject.anotherString is not being updated.Griggs
Ah! My bad. I missed the inheritance. :-) I may have an answer. Let me try something and I'll post something in a few minutes.Nonpayment
G
49

Finally figured out a solution/workaround to this issue. If you remove the property wrapper from the subclass, and call the baseclass objectWillChange.send() on the variable the state is updated properly.

NOTE: Do not redeclare let objectWillChange = PassthroughSubject<Void, Never>() on the subclass as that will again cause the state not to update properly.

I hope this is something that will be fixed in future releases as the objectWillChange.send() is a lot of boilerplate to maintain.

Here is a fully working example:

    import SwiftUI

    class MyTestObject: ObservableObject {
        @Published var aString: String = ""

    }

    class MyInheritedObject: MyTestObject {
        // Using @Published doesn't work on a subclass
        // @Published var anotherString: String = ""

        // If you add the following to the subclass updating the state also doesn't work properly
        // let objectWillChange = PassthroughSubject<Void, Never>()

        // But if you update the value you want to maintain state 
        // of using the objectWillChange.send() method provided by the 
        // baseclass the state gets updated properly... Jaayy!
        var anotherString: String = "" {
            willSet { self.objectWillChange.send() }
        }
    }

    struct MyTestView: View {
        @ObservedObject var myTestObject = MyTestObject()
        @ObservedObject var myInheritedObject = MyInheritedObject()

        var body: some View {
            NavigationView {
                VStack(alignment: .leading) {
                    TextField("Update aString", text: self.$myTestObject.aString)
                    Text("Value of aString is: \(self.myTestObject.aString)")

                    TextField("Update anotherString", text: self.$myInheritedObject.anotherString)
                    Text("Value of anotherString is: \(self.myInheritedObject.anotherString)")
                }
            }
        }
    }
Griggs answered 23/8, 2019 at 6:8 Comment(9)
Did you file this as a bug? Could you post the rdar number here so I may dupe?Perfunctory
OH. MY. GOD. I've been tearing my hair out for like a week and refactorying every which way. This. This is the problem. THANK YOU SOOOOOO MUCH!!!!Santiagosantillan
I can finally answer my question too! #60149044Santiagosantillan
Is it a bug? This is a complete PITALoan
There is another requirement to make this answer work: The base class needs at least one @Published var, otherwise objectWillChange.send() doesn't do anything.Dahliadahlstrom
Solution works, thank you so much. Debugging to get to this issue was not fun.Candra
Damn, just stumbled upon the same problem. What a mess SwiftUI is right now, if you want to make a real app and not some clickbaity tutorial.Femineity
I don't which is more annoying - the fact that this isn't fixed after almost two years, or that there is not even a comment in the docs to save people the 1000's of wasted hours.Galacto
Hi, seems ive asked a similar/duplicate question, but cant resolve it, can you see what im doing wrong? #72409284Olag
M
17

iOS 14.5 resolves this issue.

Combine

Resolved Issues

Using Published in a subclass of a type conforming to ObservableObject now correctly publishes changes. (71816443)

Moskowitz answered 21/4, 2021 at 5:43 Comment(1)
Good to know that this finally works. Unfortunately this will also be a source of subtle bugs if you're not super careful with testing on older iOS releases - @Published in subclasses will work on iOS 14.5 now but will fail in the older iOS 14.* versions.Wheeze
B
4

This is because ObservableObject is a protocol, so your subclass must conform to the protocol, not your parent class

Example:

class MyTestObject {
    @Published var aString: String = ""

}

final class MyInheritedObject: MyTestObject, ObservableObject {
    @Published var anotherString: String = ""
}

Now, @Published properties for both class and subclass will trigger view events

Beers answered 17/9, 2020 at 22:53 Comment(1)
But if you want both MyTestObject and MyInheritedObject to be observed in different Views, this solution doesn't help, since MyTestObject no longer conforms to ObservableObject.Sibilla
D
2

UPDATE

This has been fixed in iOS 14.5 and macOS 11.3, subclasses of ObservableObject will correctly publish changes on these versions. But note that the same app will exhibit the original issues when run by a user on any older minor OS version. You still need the workaround below for any class that is used on these versions.


The best solution to this problem that I've found is as follows:

Declare a BaseObservableObject with an objectWillChange publisher:

open class BaseObservableObject: ObservableObject {
    
    public let objectWillChange = ObservableObjectPublisher()

}

Then, to trigger objectWillChange in your subclass, you must handle changes to both observable classes and value types:

class MyState: BaseObservableObject {

    var classVar = SomeObservableClass()
    var typeVar: Bool = false {
        willSet { objectWillChange.send() }
    }
    var someOtherTypeVar: String = "no observation for this"

    var cancellables = Set<AnyCancellable>()

    init() {
        classVar.objectWillChange // manual observation necessary
            .sink(receiveValue: { [weak self] _ in
                self?.objectWillChange.send()
            })
            .store(in: &cancellables)
    }
}

And then you can keep on subclassing and add observation where needed:

class SubState: MyState {

    var subVar: Bool = false {
        willSet { objectWillChange.send() }
    }

}

You can skip inheriting BaseObservableObject in the root parent class if that class already contains @Published variables, as the publisher is then synthesized. But be careful, if you remove the final @Published value from the root parent class, all the objectWillChange.send() in the subclasses will silently stop working.

It is very unfortunate to have to go through these steps, because it is very easy to forget to add observation once you add a variable in the future. Hopefully we will get a better official fix.

Dahliadahlstrom answered 22/12, 2020 at 13:31 Comment(0)
S
1

This issue still exist on iOS 15/16/17. Check this example

import SwiftUI
import Combine

class SubModel: ObservableObject {
    @Published var count = 0
}

class AppModel: ObservableObject {
    @Published var submodel: SubModel = SubModel()
    
    var anyCancellable: AnyCancellable? = nil
    
// You have to enable following to make it work    
//    init() {
//            anyCancellable = submodel.objectWillChange.sink { [weak self] (_) in
//                self?.objectWillChange.send()
//            }
//        } 
//    
    
}

struct ContentView: View {
    @State private var bag = Set<AnyCancellable>()
    @StateObject private var state = AppModel()
    var body: some View {
        NavigationView {
            VStack(spacing: 30) {
                Text("xxx \(state.submodel.count)")
                Button("test") {
                    state.submodel.count += 1
                }
            }
            .onAppear {

            }
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

Suicidal answered 11/11, 2023 at 20:14 Comment(0)
C
0

This happens also when your class is not directly subclass ObservableObject:

class YourModel: NSObject, ObservableObject {

    @Published var value = false {
        willSet {
            self.objectWillChange.send()
        }
    }
}
Corporal answered 4/6, 2020 at 10:39 Comment(0)
S
-2

From my experience, just chain the subclass objectWillChange with the base class's objectWillChange like this:

class GenericViewModel: ObservableObject {
    
}

class ViewModel: GenericViewModel {
    @Published var ...
    private var cancellableSet = Set<AnyCancellable>()
    
    override init() {
        super.init()
        
        objectWillChange
            .sink { super.objectWillChange.send() }
            .store(in: &cancellableSet)
    }
}
Serdab answered 10/10, 2020 at 16:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.