Binding updating after State in SwiftUI, triggering 2 updates

I've noticed that if a Binding and a State are changed in the same withAnimation block, the State will be updated first, trigger a redraw, and then the Binding will be updated, triggering a second redraw. When instead of a Binding, I use two State variables, then the redraw happens just once and both values are updated at the same time:
Code Block
struct ContentView: View {
@Binding var x1: Int
// @State var x1: Int
@State var x2: Int = 0
var body: some View {
VStack {
Button(action: {
withAnimation {
x1 += 1
x2 += 2
}
}, label: {
Text("Increment")
.padding()
})
Text("Total: \(total)")
}
}
var total: Int {
print("\(x1) + \(x2) = \(x1 + x2)")
return x1 + x2
}
}

When hitting the button, the console prints:
Code Block
0 + 2 = 2
1 + 2 = 3

If I switch and use two State variables, I only see one print:
Code Block
1 + 2 = 3

Why is this happening? Is there a way to keep using a Binding and a State but trigger a redraw just once?
The first question I have is about that Binding. Since this is the ContentView (presumably the first view of your app), what State is it being bound to? I didn't know, so based on the fact that it was a bind, I rewrote it to include another view (compacted the code to make it smaller):

Code Block Swift
struct OtherView: View {
    @Binding var x1: Int
    @State var x2: Int = 0
    var body: some View {
        VStack {
            Button(action: {
                withAnimation { x1 += 1; x2 += 2 }
            }, label: { Text("Increment").padding() })
            Text("Total: \(total)")
        }
    }
    var total: Int { print("\(x1) + \(x2) = \(x1 + x2)"); return x1 + x2 }
}
struct ContentView: View {
    @State private var value: Int = 0
    var body: some View {
        OtherView(x1: $value)
    }
}


With this, it doesn't update the view twice, and only prints, 1 + 2 = 3.

In regards to your question about using a Binding and a State, this is possible (as shown above), but I would need to see your other view that the Binding is being connected to. If this doesn't answer your question, can you please expand/explain why you are using a Binding without binding it to something.
Thanks for your answer @Tylor. I'm trying to reproduce a situation I've noticed in an open-source library that I created so the code you see here is not Production code per se. It's just some dummy code that reproduces the issue.

I've spent some time working on your example to make it look more similar to the code where I noticed this strange behavior. If you copy-paste this into a project, you'll reproduce (use ContentView as your first view):
Code Block
struct OtherView: View {
let size: CGSize
@Binding var page: Int
@State var draggingOffset: CGFloat = 0
var body: some View {
Text(text())
.frame(width: size.width,
height: size.height / 2)
.background(Color.blue)
.gesture(
DragGesture(minimumDistance: 15)
.onChanged { value in
withAnimation {
draggingOffset = value.translation.width
}
}
.onEnded { _ in
print("\n\n* RESET **")
withAnimation {
page += 1
draggingOffset = 0
}
}
)
}
func text() -> String {
print("page: \(page) offset: \(draggingOffset)")
return "page: \(page) offset: \(draggingOffset)"
}
}
struct ContentView: View {
@State private var value: Int = 0
var body: some View {
NavigationView {
GeometryReader { proxy in
OtherView(size: proxy.size,
page: $value)
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}

If you use this code you'll see this prints something similar to:
Code Block
page: 0 offset: 0.0
...
...
page: 0 offset: -239.3333282470703
* RESET **
page: 1 offset: -239.3333282470703
page: 1 offset: 0.0

Notice how the state is updated in two steps: first page increments and then draggingOffset resets. The reason why I have a GeometryReader in ContentView is because I need to know the space available to make some calculations. I have a bunch of variables depending on this size so by having a proxy view I can have the size as a dependency so I don't need to be passing as an argument to make these calculations.

If you take that GeometryReader into OtherView and remove the dependency then you'll see how the console prints differently:
Code Block
* RESET **
page: 1 offset: 0.0

If you remove the NavigationView, then the log shows a correct behavior and prints the same as above. So this makes me think there's something wrong in this combination NavigationView, GeometryReader and proxy view.

Binding updating after State in SwiftUI, triggering 2 updates
 
 
Q