hanamaruby
hanamaruby

Reputation: 93

SwiftUI @Binding value can not change and called init

I want to make a picker use SwiftUI, when I change the value in ChildView, it will not change and called ChildView init.

class ViewModel: ObservableObject {
    @Published var value: Int
    
    init(v: Int) {
        self.value = v
    }
}

struct ChildView: View {
    @Binding var value: Int
    @ObservedObject var vm = ViewModel(v: 0)
    
    init(value: Binding<Int>) {
        self._value = value
        print("ChildView init")
    }
    
    var body: some View {
        VStack {
            Text("param value: \(value)")
            Text("@ObservedObject bar: \(vm.value)")
            Button("(child) bar.value++") {
                self.vm.value += 1
            }
        }
        .onReceive(vm.$value) { value = $0 }
    }
}

struct ContentView: View {
    @State var value = 0
    
    var body: some View {
        VStack {
            Text("(parent) \(self.value)")
            ChildView(value: $value)
        }
    }
}

But when I remove Text("(parent) \(self.value)") in ContentView, it seems to be normal.

Upvotes: 2

Views: 2178

Answers (2)

Asperi
Asperi

Reputation: 257493

In general, the described behavior is expected, because source of truth for value is in parent, and updating it via binding you update all places where it is used. That result in rebuild parent body, so recreate child view.

SwiftUI 2.0

Solution is simple - use state object

struct ChildView: View {
    @Binding var value: Int
    @StateObject var vm = ViewModel(v: 0)   // << here !!

   // ... other code

SwiftUI 1.0+

Initialize view model with updated bound value

struct ChildView: View {
    @Binding var value: Int
    @ObservedObject var vm: ViewModel    // << declare !!

    init(value: Binding<Int>) {
        self._value = value
        self.vm = ViewModel(v: value.wrappedValue) // << initialize !!

    // .. other code

Upvotes: 1

New Dev
New Dev

Reputation: 49580

This happens because anytime ChildView gets init-ialized - which happens when ContentView's body is recomputed - it create a new instance of ViewModel with the value 0.

Determine first who "owns" the data. If it's some external object, like ViewModel, then it should get instantiated somewhere where an instance could be longer-lived, for example in ContentView (but it would depend on your real use case):

struct ContentView: View {
    @State var value = 0
    var childVm = ViewModel(v: 0)
    
    var body: some View {
        VStack {
            Text("(parent) \(self.value)")
            ChildView(vm: childVm, value: $value)
        }
    }
}
struct ChildView: View {
    @Binding var value: Int
    @ObservedObject var vm: ViewModel
    
    init(vm: ViewModel, value: Binding<Int>) {
        self._value = value
        self.vm = vm
        print("ChildView init")
    }
    // ...
}

Upvotes: 2

Related Questions