1

Consider the following code:

class Model: ObservableObject {
    @Published var property1: Int = 0
    @Published var property2: Int = 0
}

struct ObjectBindingTest: View {
    @StateObject private var model = Model()
    
    var body: some View {
        print("——— top")
        return VStack(spacing: 30) {
            SomeSimpleComponent(property: $model.property1)
            SomeSimpleComponent2(property: $model.property2)
        }
        .padding(50)
    }
    
}

struct SomeSimpleComponent: View {
    @Binding var property: Int
    
    var body: some View {
        print("component 1")
        return HStack {
            Text("\(property)")
            Button("Increment", action: { property += 1 })
        }
    }
}

struct SomeSimpleComponent2: View {
    @Binding var property: Int
    
    var body: some View {
        print("component 2")
        return HStack {
            Text("\(property)")
            Button("Increment", action: { property += 1 })
        }
    }
}

Whenever you press on one of the buttons, you will see in console:

——— top
component 1
component 2

Meaning that all body blocks get evaluated.

I would expect that only the corresponding row gets updated: if I press the first button and therefore update property1, the second row shouldn't have to re-evaluate its body because it's only dependent on property2.

This is causing big performance issues in my app. I have a page to edit an object with many properties. I use an ObservableObject with many @Published properties. Every time a property changes (for instance while typing in a field), all the controls in the page get updated, which causes lags and freezes. The performance issues mostly happen in iOS 14; I'm not sure whether they're not happening in iOS 15 or if it's just that the device has more computing power.

How to prevent unnecessary updates coming from ObservableObject, and only update the views that actually watch the updated property?

Morpheus
  • 1,189
  • 2
  • 11
  • 33
  • View struct updates are unlikely to cause lags themselves because they are just values, it's more likely that something is causing UIView objects to be unnecessary created instead of reused. Are you messing with View identities anywhere? – malhal Dec 09 '22 at 18:49
  • How to know if the underlying UIViews were recreated instead of reused? – Morpheus Dec 13 '22 at 08:59

1 Answers1

2

The behavior you are seeing is expected

By default an ObservableObject synthesizes an objectWillChange publisher that emits the changed value before any of its @Published properties changes.

In other words all the wrappers trigger a single publisher so SwiftUI does not know which was updated.

https://developer.apple.com/documentation/combine/observableobject

You can get a partial performance upgrade by changing from a class to a struct and using @State

    struct Model {
        var property1: Int = 0
        var property2: Int = 0
    }

    @State private var model = Model()

In certain cases such a ForEach you will get improvements by adding a few protocols.

    struct Model: Equatable, Hashable, Identifiable {
        let id: UUID = .init()
        //More code

Check out Demystify SwiftUI from #wwdc21 https://developer.apple.com/wwdc21/10022 it will provide a greater insight into the why.

lorem ipsum
  • 21,175
  • 5
  • 24
  • 48
  • Using a @State would not solve the problem because all views would still be updated for any change – Morpheus Dec 08 '22 at 15:25
  • @Morpheus like I siad, it would only be a partial improvement. If you notice in your code "top" doesn't get called anymore, just the subviews. – lorem ipsum Dec 08 '22 at 15:27