41

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)")
            }
        }
    }
}
thiezn
  • 1,874
  • 1
  • 17
  • 24
  • Not to be offending, but your posted code doesn't contain anything to suggest subclassing. Where is it? –  Aug 22 '19 at 19:48
  • 3
    I might be mixing terms but the class MyInheritedObject inherits from class MyTestObject? – thiezn Aug 22 '19 at 19:53
  • 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. – thiezn Aug 22 '19 at 20:31
  • 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. –  Aug 22 '19 at 21:20

6 Answers6

48

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)")
                }
            }
        }
    }
thiezn
  • 1,874
  • 1
  • 17
  • 24
  • 3
    Did you file this as a bug? Could you post the rdar number here so I may dupe? – Reneli Dec 27 '19 at 22:23
  • 6
    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!!!! – Othyn Feb 11 '20 at 10:37
  • I can finally answer my question too! https://stackoverflow.com/questions/60148544/swiftui-custom-views-viewbuilder-doesnt-re-render-update-on-subclassed-observe – Othyn Feb 11 '20 at 11:02
  • 1
    Is it a bug? This is a complete PITA – elight May 28 '20 at 18:06
  • 3
    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. – Oskar Aug 04 '20 at 00:43
  • Solution works, thank you so much. Debugging to get to this issue was not fun. – Robert Penner Mar 02 '21 at 19:15
  • 1
    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. – Joris Mans Mar 18 '21 at 15:47
  • 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. – Rob N Apr 12 '21 at 00:05
  • Hi, seems ive asked a similar/duplicate question, but cant resolve it, can you see what im doing wrong? https://stackoverflow.com/questions/72408784/published-var-not-re-painting-view-when-set-in-ios-14 – Wazza May 27 '22 at 17:52
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)

grg
  • 5,023
  • 3
  • 34
  • 50
  • 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. – Ralf Ebert May 01 '21 at 21:03
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

ColinMasters
  • 511
  • 7
  • 19
  • 2
    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`. – Andrew Bennet Jan 22 '21 at 21:33
3

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.

Oskar
  • 3,625
  • 2
  • 29
  • 37
0

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

class YourModel: NSObject, ObservableObject {

    @Published var value = false {
        willSet {
            self.objectWillChange.send()
        }
    }
}
Olcay Ertaş
  • 5,987
  • 8
  • 76
  • 112
-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)
    }
}