SwiftUI view updates even if it does not depend on a dynamic property

Viewed 429

I created an ObservableObject that gathers data and a view that depends on this object. More specifically some parts of the UI depend on one property of it and other parts depend on other properties.

The way ObservableObject works is that if any of its Published property gets updated (event if it does not change) it sends an objectWillChange notification that triggers updates for subscribed views.

However, I did expect that only the parts of the views that depends on an ObservableObject property that actually changed would be updated, not the entire view because body computations are expensive.

Unfortunately this it not the behavior I observed during my experiments. For instance in the following code the List view depends only on the color dynamic property of the ObservedObject "preferences" and the TextField updates only the text property of the observed object. I observed with the debugPrint's that when I type text the List and its rows gets updated each time even if it does not depend on text.

struct ContentView: View {
    @State private var list = ["foo", "bar", "baz"]
    @StateObject private var preferences = Preferences()
    
    var body: some View {
        VStack {
            List(list, id: \.self) { element in
                Text(element)
                    .foregroundColor(preferences.color)
                    .debugPrint("Row view updated")
            }
            
            Spacer()
            
            HStack {
                TextField("Text", text: $preferences.text)
                    .debugPrint("Text field view updated")
                
                Button("Toogle Color", action: toggleColor)
                    .debugPrint("Button view updated")
            }
        }
        .padding()
        .debugPrint("Content view updated")
    }
    
    func toggleColor() {
        preferences.color = [.blue, .green, .orange, .red].randomElement()!
    }
}

final class Preferences: ObservableObject {
    @Published var color: Color = .blue
    @Published var text: String = ""
}

extension View {
    func debugPrint(_ elements: Any...) -> Self {
        #if DEBUG
        print(elements)
        return self
        #else
        return self
        #endif
    }
}

Is this the correct and intended behavior? How the optimal behavior I described above could be obtained, i.e. not calling body when the ObservedObject changes but only the components that depends on specific properties?

I observed in bigger SwiftUI projects that this behavior can greatly slow down the application and is not easily debuggable because unique state is often enforced through heavy ObservedObjects (or StateObject) injected at the root view and used in many places. I Observed this behavior even with small lists of components with heavy UI layout.

Note 1: I was bugged by the StateObject definition which suggests that only the views that depend on those properties are updated (and not when the StateObject changes).

SwiftUI creates a new instance of the object only once for each instance of the structure that declares the object. When published properties of the observable object change, SwiftUI updates the parts of any view that depend on those properties [...]

Note 2: The issue totally disappear if I define the List in its own component, the rendering is fast:

struct ListView: View {
    let list: [String]  // Also works with a @Binding
    
    let color: Color  // Also works if it would be `preferences`
    
    var body: some View {
        List {
            ForEach(list, id: \.self) { element in
                Row(text: element, color: color)
            }
        }
    }
}

Why in this case the view updates are fast?

0 Answers
Related