iosswiftuiproperty-wrapper

What is the difference between (@StateObject & @StateObject) and (@StateObject & @ObservedObject) in Parent and Child Views


@StateObject & @StateObject in Both Parent and ChildView

import SwiftUI


class Counter: ObservableObject {
    @Published var count: Int = 0
    
    func increment() {
        count += 1
    }
}

struct ContentView: View {
    @StateObject private var counter : Counter = Counter()
    
    var body: some View {
        VStack {
            Text("Parent Counter: \(counter.count)")
            Button("Increment Child Counter") {
                counter.increment()
            }
            ChildView(counter: counter)
        }
    }
}

struct ChildView: View {
    @StateObject var counter: Counter
    
    var body: some View {
        VStack {
            Text("Child Counter: \(counter.count)")
            Button("Increment Child Counter") {
                counter.increment()
            }
        }
    }
}

struct RandomNumberView: View {
    @State var randomNumber = 0

    var body: some View {
        VStack {
            Text("Random number is: \(randomNumber)")
            Button("Randomize number") {
                randomNumber = (0..<1000).randomElement()!
            }
        }.padding(.bottom)
        
        ContentView()
    }
}


#Preview {
    RandomNumberView()
}

@StateObject & @ObservedObject in Parent and ChildView

import SwiftUI


class Counter: ObservableObject {
    @Published var count: Int = 0
    
    func increment() {
        count += 1
    }
}

struct ContentView: View {
    @StateObject private var counter : Counter = Counter()
    
    var body: some View {
        VStack {
            Text("Parent Counter: \(counter.count)")
            Button("Increment Child Counter") {
                counter.increment()
            }
            ChildView(counter: counter)
        }
    }
}

struct ChildView: View {
    @ObservedObject var counter: Counter
    
    var body: some View {
        VStack {
            Text("Child Counter: \(counter.count)")
            Button("Increment Child Counter") {
                counter.increment()
            }
        }
    }
}

struct RandomNumberView: View {
    @State var randomNumber = 0

    var body: some View {
        VStack {
            Text("Random number is: \(randomNumber)")
            Button("Randomize number") {
                randomNumber = (0..<1000).randomElement()!
            }
        }.padding(.bottom)
        
        ContentView()
    }
}


#Preview {
    RandomNumberView()
}

Both Having the same Output View.

I have checked many of the articles and my own cases in @StateObject vs @ObservedObject in childView But I don't find anything useful. So what's the difference in this case. I want the explanation for this to code how it works when @StateObject & @ObservedObject used in childView.


Solution

  • When SwiftUI updates a View that has already been seen before, it restores the values of that view's @State and @StateObject properties to the values they had on the prior rendering.

    So, the first time SwiftUI asks ContentView for its body, ContentView creates a ChildView and passes its (ContentView's) counter to initialize ChildView's counter.

    When counter's @Published count property changes, SwiftUI asks ContentView for its body again. ContentView.body creates another ChildView and again passes its counter down to initialize ChildView's counter.

    However, SwiftUI recognizes that this ChildView represents an update to the prior ChildView. So it throws away whatever was stored in this new ChildView's counter and sets it to the Counter that was used the first time it saw this ChildView.

    This doesn't make a difference in your example, because you only ever create one Counter.

    Let's write a example where it does matter.

    First, here's a simple Button that displays a count and increments the count when tapped, just using a Binding:

    struct CountButton: View {
        @Binding var count: Int
        var body: some View {
            Button {
                count += 1
            } label: {
                Text("\(count)")
                    .frame(width: 60)
            }
        }
    }
    

    Now here's a view that uses a StateObject to hold a Counter and pass its count to a CountButton:

    struct ViewWithStateObject: View {
        @StateObject var counter: Counter
    
        var body: some View {
            GroupBox("View with StateObject") {
                CountButton(count: $counter.count)
                    .frame(width: 300)
                    .padding()
            }
            .padding()
        }
    }
    

    And here's a similar view that uses an ObservedObject instead:

    struct ViewWithObservedObject: View {
        @ObservedObject var counter: Counter
    
        var body: some View {
            GroupBox("View with ObservedObject") {
                CountButton(count: $counter.count)
                    .frame(width: 300)
                    .padding()
            }
            .padding()
        }
    }
    

    And finally, here's a view that holds two Counter instances, each in a StateObject. It embeds both of the above views, and lets you choose which of its Counter instances to pass to both children:

    struct ContentView: View {
        @StateObject private var counter0: Counter = Counter()
        @StateObject private var counter1: Counter = Counter()
        @State var useCounter1 = false
    
        var body: some View {
            VStack {
                GroupBox("ContentView") {
                    VStack {
                        HStack {
                            CountButton(count: $counter0.count)
                            CountButton(count: $counter1.count)
                        }
                        Toggle("Pass counter1 to children", isOn: $useCounter1)
                    }
                    .frame(width: 300)
                    .padding()
                }
    
                ViewWithStateObject(counter: useCounter1 ? counter1 : counter0)
                ViewWithObservedObject(counter: useCounter1 ? counter1 : counter0)
            }
            .padding()
        }
    }
    

    If you tap the top left CountButton in this example, you'll find that both children are updated as the count increases. And the buttons in the children also increment the same count. They are all using counter0.

    If you tap the top right CountButton, only it updates. The other buttons are not affected, because only that top right button uses counter1.

    Next, tap the toggle. This makes ContentView pass counter1 to the children.

    Now, when you tap the top left button, both the top left button and the “View with StateObject” show the increment, but the “View with ObservedObject” button doesn't change. And if you tap the top right button, both it and the “View with ObservedObject” button show the increment, but the “View with StateObject” button doesn't change.

    When you turn on the toggle, ContentView passes counter1 to the children, instead of passing counter0. But, before SwiftUI asks ViewWithStateObject for its body, SwiftUI discards the contents of ViewWithStateObject's counter property and replaces it with the prior contents. So it discards the new reference to counter1 and restores the old reference to counter0. ViewWithStateObject will never switch to counter1.