user2742293
user2742293

Reputation: 59

Change on ObservableObject in @Published array does not update the view

I've been struggling for hours on an issue with SwiftUI.

Here is a simplified example of my issue :

class Parent: ObservableObject {
    @Published var children = [Child()]
}

class Child: ObservableObject {
    @Published var name: String?

    func loadName() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            // Async task here...
            self.objectWillChange.send()
            self.name = "Loaded name"
        }
    }

}

struct ContentView: View {
    @ObservedObject var parent = Parent()

    var body: some View {
        Text(parent.children.first?.name ?? "null")
            .onTapGesture {
                self.parent.objectWillChange.send()
                self.parent.children.first?.loadName() // does not update
            }
    }
}

I have an ObservableObject (Parent) storing a @Published array of ObservableObjects (Child).

The issue is that when the name property is changed via an async task on one object in the array, the view is not updated.

Do you have any idea ?

Many thanks Nicolas

Upvotes: 5

Views: 6532

Answers (3)

SwiftiSwift
SwiftiSwift

Reputation: 8687

Make sure your Child model is a struct! Classes doesn't update the UI properly.

Upvotes: 2

Asperi
Asperi

Reputation: 257493

I would say it is design issue. Please find below preferable approach that uses just pure SwiftUI feature and does not require any workarounds. The key idea is decomposition and explicit dependency injection for "view-view model".

Tested with Xcode 11.4 / iOS 13.4

demo

class Parent: ObservableObject {
    @Published var children = [Child()]
}

class Child: ObservableObject {
    @Published var name: String?

    func loadName() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            // Async task here...
            self.name = "Loaded name"
        }
    }
}

struct FirstChildView: View {
    @ObservedObject var child: Child
    var body: some View {
        Text(child.name ?? "null")
            .onTapGesture {
                self.child.loadName()
            }
    }
}

struct ParentContentView: View {
    @ObservedObject var parent = Parent()

    var body: some View {
        // just for demo, in real might be conditional or other UI design
        // when no child is yet available
        FirstChildView(child: parent.children.first ?? Child())
    }
}

Upvotes: 2

this alternative approach works for me:

class Parent: ObservableObject {
@Published var children = [Child()]
}

class Child: ObservableObject {
@Published var name: String?

func loadName(handler: @escaping () -> Void) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        // Async task here...
        self.name = UUID().uuidString  // just for testing
        handler()
    }
}
}

struct ContentView8: View {
@ObservedObject var parent = Parent()
var body: some View {
    Text(parent.children.first?.name ?? "null").padding(10).border(Color.black)
        .onTapGesture {
            self.parent.children.first?.loadName(){
                self.parent.objectWillChange.send()
            }
    }
}
} 

Upvotes: -1

Related Questions