Muhand Jumah
Muhand Jumah

Reputation: 1958

@Published doesn't work as expected with enviornment object and nested ObservableObejct

Say I have UserSettings EnviornmentObject and one of it's properties is a class, the problem is when I change a value of that class, the EnviornmentObject won't publish these changes. I understand why, but I can't seem to find a workaround.

Here is a simplified code to show the problem:

struct TestView: View {
    @EnvironmentObject var settings: UserSettings

    var body: some View {
        ZStack {
            Text("test: \(self.settings.ob.val)")

            VStack {
                // This is the only one that works and that makes sense, it changes the entire object
                Button(action: {
                    self.settings.changeOb(to: testOb(val: "1"))
                }) {
                    Text("Change object")
                }

                // From here on nothing works, I tried different ways to change the object value

                Button(action: {
                    self.settings.ob.changeVal(to: "2")
                }) {
                    Text("Change object's val")
                }

                Button(action: {
                    self.settings.changeVal(to: "3")
                }) {
                    Text("Change object's val V2")
                }

                Spacer()
            }
        }
    }
}

struct TestView_Previews: PreviewProvider {
    static var previews: some View {
        return ZStack {
            TestView().environmentObject(UserSettings(ob: testOb("abc")))
        }
    }
}

class testOb: ObservableObject {
    @Published private(set) var val: String

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

    func changeVal(to: String) {
        self.val = to
    }
}

class UserSettings: ObservableObject {
    @Published private(set) var ob: testOb

    init(ob: testOb) {
        self.ob = ob
    }

    func changeOb(ob: testOb) {
        self.ob = ob
    }


    func changeVal(to: String) {
        self.ob.val(to: to)
    }
}

Upvotes: 0

Views: 203

Answers (2)

Aspid
Aspid

Reputation: 699

try something like this:

class UserSettings: ObservableObject {
    @Published var ob: testOb{
        willSet{
            observer.cancel()
        }
        didSet{
            observer = ob.objectWillChange.sink(){self.objectWillChange.send()}
        }
    }
    var observer: AnyCancellable!

    init(ob: testOb) {
        self.ob = ob
        self.observer = nil
        self.observer = ob.objectWillChange.sink(){self.objectWillChange.send()}
    }
    func sendChange(){

    }
    func changeOb(ob: testOb) {
        self.ob = ob
    }

    func changeVal(to: String) {
        self.ob.changeVal(to: to)
    }
    deinit {
        observer.cancel()
    }
}

@Published is for sending notifications, not for listening.

Or you can store a weak link to parent in child object and call parent.objectWillChange.send() in child's willSet{}.

Upvotes: 1

Muhand Jumah
Muhand Jumah

Reputation: 1958

I finally was able to fix it, the problem is that Apple doesn't support nested ObesrvableObjects along with many important features (which is sad but whatever) yet but as I heard, it's on their agenda.

The solution is that I have to manually notify everytime my object will change and this could be painful the more objects you have.

To notify we first need to import Combine so we can use AnyCancellable which is responsible for notifications.

Here is the updated code:

    // FIRST CHANGE
    import Combine

    struct TestView: View {
        @EnvironmentObject var settings: UserSettings

        var body: some View {
            ZStack {
                Text("test: \(self.settings.ob.val)")

                VStack {
                    // This is the only one that works and that makes sense, it changes the entire object
                    Button(action: {
                        self.settings.changeOb(to: testOb(val: "1"))
                    }) {
                        Text("Change object")
                    }

                    // From here on nothing works, I tried different ways to change the object value

                    Button(action: {
                        self.settings.ob.changeVal(to: "2")
                    }) {
                        Text("Change object's val")
                    }

                    Button(action: {
                        self.settings.changeVal(to: "3")
                    }) {
                        Text("Change object's val V2")
                    }

                    Spacer()
                }
            }
        }
    }

    struct TestView_Previews: PreviewProvider {
        static var previews: some View {
            return ZStack {
                TestView().environmentObject(UserSettings(ob: testOb("abc")))
            }
        }
    }

    class testOb: ObservableObject {
        @Published private(set) var val: String

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

        func changeVal(to: String) {
            self.val = to
        }
    }

    class UserSettings: ObservableObject {
        @Published private(set) var ob: testOb

        // SECOND CHANGE, this is responsible to notify on changes
        var anyCancellable: AnyCancellable? = nil

        init(ob: testOb) {
            self.ob = ob

            // THIRD CHANGE, initialize our notifier
            anyCancellable = self.ob.objectWillChange.sink { (_) in
               self.objectWillChange.send()
            }
        }

        func changeOb(ob: testOb) {
            self.ob = ob
        }


        func changeVal(to: String) {
            self.ob.val(to: to)
        }
    }

Upvotes: 0

Related Questions