Data update issue when both parent and child views are @State in SwiftUI

Here is my test code

import SwiftUI

struct SubView1: View {
    @State var data: String

    var body: some View {
        Text(data)
        let _ = addr()
    }

    func addr() {
        let a = withUnsafePointer(to: data) { pointer in
            return"\(pointer)"
        }

        let b = withUnsafePointer(to: self) { pointer in
            return"\(pointer)"
        }

        print("SubView1, \(a), \(b), \(self)")
    }
}

struct SubView2: View {
    var data: String

    var body: some View {
        Text(data)
        let _ = addr()
    }

    func addr() {
        let a = withUnsafePointer(to: data) { pointer in
            return"\(pointer)"
        }

        let b = withUnsafePointer(to: self) { pointer in
            return"\(pointer)"
        }

        print("SubView2, \(a), \(b), \(self)")
    }
}

struct ContentView: View {
    @State var data: String = "a"

    var body: some View {
        let _ = print("ContentView")
        SubView1(data: data)
        SubView2(data: data)
        
        Button("change") {
            data = "b"
            print("changed")
        }
    }
}

Here is what is printed

ContentView
SubView1, 0x000000016ce791a8, 0x000000016ce79170, SubView1(_data: SwiftUI.State<Swift.String>(_value: "a", _location: Optional(SwiftUI.StoredLocation<Swift.String>)))
SubView1, 0x000000016ce79260, 0x000000016ce79230, SubView2(data: "a")
changed
ContentView
SubView1, 0x000000016ce7d548, 0x000000016ce7d510, SubView1(_data: SwiftUI.State<Swift.String>(_value: "a", _location: Optional(SwiftUI.StoredLocation<Swift.String>)))
SubView1, 0x000000016ce7d600, 0x000000016ce7d5d0, SubView2(data: "b")

In my understanding, @State wrapping is monitoring of variables, and when the wrapped variables change, it will trigger the update of this page. Updating the interface means recreating the body, so all subpages will be recreated.

When I click the button, the data of ContentView is updated first, which leads to the update of ContentView , and then leads to the reconstruction of SubView1 and SubView2.

Judging from the changed address, it is indeed rebuilt rather than reused.

But the problem is that the data of ContentView has been updated to "b" at this time, and SubView2 is indeed reinitialized using "b", so why does SubView1 still use "a"?

In addition, I modified another one and added a SubView1 to ContentView

struct ContentView: View {
    @State var data: String = "a"

    var body: some View {
        let _ = print("ContentView")
        SubView1(data: data)
        SubView2(data: data)
        SubView1(data: data)
        
        Button("change") {
            data = "b"
            print("changed")
        }
    }
}

The following result was obtained

ContentView
SubView1, 0x000000016d9cd1a8, 0x000000016d9cd170, SubView1(_data: SwiftUI.State<Swift.String>(_value: "a", _location: Optional(SwiftUI.StoredLocation<Swift.String>)))
SubView2, 0x000000016d9cd260, 0x000000016d9cd230, SubView2(data: "a")
SubView1, 0x000000016d9cd1a8, 0x000000016d9cd170, SubView1(_data: SwiftUI.State<Swift.String>(_value: "a", _location: Optional(SwiftUI.StoredLocation<Swift.String>)))
changed
ContentView
SubView1, 0x000000016d9d1548, 0x000000016d9d1510, SubView1(_data: SwiftUI.State<Swift.String>(_value: "a", _location: Optional(SwiftUI.StoredLocation<Swift.String>)))
SubView2, 0x000000016d9d1600, 0x000000016d9d15d0, SubView2(data: "b")
SubView1, 0x000000016d9d1548, 0x000000016d9d1510, SubView1(_data: SwiftUI.State<Swift.String>(_value: "a", _location: Optional(SwiftUI.StoredLocation<Swift.String>)))

It seems that the two SubView1 are the same object?

Answered by Claude31 in 792008022

For the first question.

You have to use Binding and not State in SubView1:

struct SubView1: View {
//    @State var data: String
    @Binding var data: String

And of course call it appropriately:

        SubView1(data: $data)

With State, you keep the state of the var. With Binding, you reference the parameter passed, so it is updated. In SubView2, you pass directly the parameter, so it is used for display.

For your second question, my understanding is that effectively the compiler optimises the views creation. As Subview1 is already created, it can reuse it immediately without creating a new instance.

Accepted Answer

This is „expected behaviour“: https://samwize.com/2024/05/08/do-not-init-state-externally-in-swiftui-view/

For the first question.

You have to use Binding and not State in SubView1:

struct SubView1: View {
//    @State var data: String
    @Binding var data: String

And of course call it appropriately:

        SubView1(data: $data)

With State, you keep the state of the var. With Binding, you reference the parameter passed, so it is updated. In SubView2, you pass directly the parameter, so it is used for display.

For your second question, my understanding is that effectively the compiler optimises the views creation. As Subview1 is already created, it can reuse it immediately without creating a new instance.

Data update issue when both parent and child views are @State in SwiftUI
 
 
Q