Fernando
Fernando

Reputation: 771

Binding and State not updating at the same time

I've noticed a scenario in SwiftUI where if I update a Binding and a State variable at the same time, the View is recomputed in 2 steps:

I've prepared a simple and dummy DEMO to reproduce this:

import SwiftUI

struct OtherView: View {
    let size: CGSize
    @Binding var x1: Int
    @State var x2: Int = 0

    var body: some View {
        Text(text())
            .frame(width: size.width,
                   height: size.height / 2)
            .background(Color.blue)
            .onTapGesture {
                print("*** TAP ***")
                withAnimation(Animation.easeIn(duration: 4)) {
                    x1 += 1
                    x2 += 1
                }
            }
    }

    func text() -> String {
        print("x1: \(x1) + x2 = \(x2) = \(x1 + x2)")
        return "x1: \(x1) + x2 = \(x2) = \(x1 + x2)"
    }
}

struct ContentView: View {
    @State private var value: Int = 0
    var body: some View {
        NavigationView {
            GeometryReader { proxy in
                OtherView(size: proxy.size,
                          x1: $value)
            }
        }
    }
}

This snippet prints:

x1: 0 + x2 = 0 = 0
x1: 0 + x2 = 0 = 0
*** TAP ***
x1: 1 + x2 = 0 = 1
x1: 1 + x2 = 1 = 2

Which is not what I expected. If I remove the NavigationView or move GeometryReader from ContentView to OtherView, the same example would print:

x1: 0 + x2 = 0 = 0
*** TAP ***
x1: 1 + x2 = 1 = 2

As expected. I've also noticed that by changing the execution order in withAnimation block I can get the State to update before the Binding.

Am I missing something? Why does the code trigger two updates?

Upvotes: 1

Views: 424

Answers (1)

lorem ipsum
lorem ipsum

Reputation: 29242

structs are immutable when you update a variable that has a SwiftUI wrapper you are telling it so reload the entire View/struct. So, when you update x1 it triggers a reload and when you update x2 you trigger another.

If you want to control when the View does a reload you have to use variables that are mutable/live in a class, and are not wrapped with a SwiftUI wrapper.

struct OtherView: View {
    let size: CGSize
    @EnvironmentObject var vm: SyncUpdateViewModel
    //@Binding var x1: Int
    @State var x2: Int = 0
    
    var body: some View {
        Text(text())
            .frame(width: size.width,
                   height: size.height / 2)
            .background(Color.blue)
            .onTapGesture {
                print("*** TAP ***")
                withAnimation(Animation.easeIn(duration: 4)) {
                    vm.x1 += 1
                    x2 += 1
                }
                
            }
    }
    
    func text() -> String {
        print("x1: \(vm.x1) + x2 = \(x2) = \(vm.x1 + x2)")
        return "x1: \(vm.x1) + x2 = \(x2) = \(vm.x1 + x2)"
    }
}
class SyncUpdateViewModel: ObservableObject {
    var x1: Int = 0
}
struct SyncUpdate: View {
    @StateObject var vm: SyncUpdateViewModel = SyncUpdateViewModel()
    //@State private var value: Int = 0
    var body: some View {
        NavigationView {
            GeometryReader { proxy in
                OtherView(size: proxy.size).environmentObject(vm)
            }
        }
    }

}

Upvotes: 1

Related Questions