Vitaliy
Vitaliy

Reputation: 21

Why does my SwiftUI View with @StateObject update unexpectedly when using a shared ObservableObject across views?

I’m working on an iOS app in SwiftUI where I have a shared ObservableObject that I want to pass between multiple views. The object is meant to keep track of some shared app state, like user settings or session data.

In one of my views, I use @StateObject to create an instance of this object. However, I noticed that any changes I make to the ObservableObject in another view cause my original view to update unexpectedly. It’s as if the object is being observed in multiple places, even though I thought @StateObject would keep it isolated in the view where it’s declared.

Here’s a simplified version of my code:

import SwiftUI

class SharedData: ObservableObject {
    @Published var counter: Int = 0
}

struct ParentView: View {
    var body: some View {
        VStack {
            CounterView()
            AnotherView()
        }
    }
}

struct CounterView: View {
    @StateObject private var data = SharedData()
    
    var body: some View {
        VStack {
            Text("Counter: \(data.counter)")
            Button("Increment Counter") {
                data.counter += 1
            }
        }
    }
}

struct AnotherView: View {
    @ObservedObject var data = SharedData()  // <- Issue?
    
    var body: some View {
        Text("Another View Counter: \(data.counter)")
    }
}

When I increment the counter in CounterView, AnotherView also reflects the changes in data.counter. I don’t understand why AnotherView is updating when CounterView increments data.counter, as I thought each view would manage its own instance.

Upvotes: 1

Views: 96

Answers (1)

ITGuy
ITGuy

Reputation: 715

Properties that are provided with the ObservedObject property wrapper should never have a default or initial value.

Apple is also very clear about this in the documentation:

Don’t specify a default or initial value for the observed object. Use the attribute only for a property that acts as an input for a view, as in the above example.

ObservedObject wrapped properties should therefore always be injected into the view as a dependency and never be created in the view itself.

The background to this is that otherwise, every time the view is recreated, a new instance of your ObservableObject conforming model instance is created. This is exactly what you don't want if you want to share the state between views / view instances.

So in your code you could do the following if you want to use one instance of SharedData for both views:

struct ParentView: View {
    @StateObject private var data = SharedData()

    var body: some View {
        VStack {
            CounterView(data: data)
            AnotherView(data: data)
        }
    }
}

struct CounterView: View {
    @ObservedObject var data: SharedData
    
    var body: some View {
        VStack {
            Text("Counter: \(data.counter)")
            Button("Increment Counter") {
                data.counter += 1
            }
        }
    }
}

struct AnotherView: View {
    @ObservedObject var data: SharedData
    
    var body: some View {
        Text("Another View Counter: \(data.counter)")
    }
}

Or if you want to use a separate instance for each view:

struct ParentView: View {
    var body: some View {
        VStack {
            CounterView()
            AnotherView()
        }
    }
}

struct CounterView: View {
    @StateObject private var data = SharedData()
    
    var body: some View {
        VStack {
            Text("Counter: \(data.counter)")
            Button("Increment Counter") {
                data.counter += 1
            }
        }
    }
}

struct AnotherView: View {
    @StateObject private var data = SharedData()
    
    var body: some View {
        Text("Another View Counter: \(data.counter)")
    }
}

Upvotes: 1

Related Questions