swiftuibindingdidset

SwiftUI @Binding how to confirm the source is updating


Here's an example that demonstrates what I'm seeing:

struct Tapper: View {
    @Binding var incrementMe: Int
    var body: some View {
        Button("Button With Binding") {
            incrementMe += 1
        }
    }
}
struct ContentView: View {
    @State private var tapCount: Int = 0 {
        didSet{
            print("didSet tapCount = \(tapCount)")
        }
    }
    var body: some View {
        VStack {
            Text("tapCount: \(tapCount)")
            Tapper(incrementMe: $tapCount)
                .padding(4.0)
            Button("Button Without Binding") {
                tapCount += 1
            }
        }
        .padding()
    }
}

ContentView renders this:

enter image description here

"Button With Binding" is the button from the Tapper struct with the binding back to tapCount. "Button Without Binding" is the button from ContentView that increments tapCount directly (no binding). As far as the view is concerned both buttons appear to work exactly as I would expect.

However, didSet is only called for the "Button Without Binding" button. didSet is never called for the "Button With Binding" button.

Clearly, tapCount is being updated from "Button With Binding". When I tap it a few times and then tap "Button Without Binding" tapCount increments from its previous value correctly.

Why isn't didSet called from the binding? Without didSet how can you confirm that a binding is indeed flowing all the way back to the source?


Solution

  • Since the View struct is immutable you are really applying the didSet to the property wrapper so it only works for that state wrapper and not any other wrappers like the binding one. So, you could just implement didSet again for your @Binding, e.g.

    struct Tapper: View {
        @Binding var incrementMe: Int { 
            didSet{
                print("didSet incrementMe = \(incrementMe)")
            }
        }
        var body: some View {
            Button("Button With Binding") {
                incrementMe += 1
            }
        }
    }
    

    To have the same code run in both didSets you could simply call a func or to have exactly the same code path just move the tapCount into a struct, e.g.

    struct Content {
        var tapCount: Int = 0 {
            didSet{
                print("didSet tapCount = \(tapCount)")
            }
        }
    
        // its nice to make funcs for testable logic
        mutating func increment() {
            tapCount += 1
        }
    }
    
    struct Tapper: View {
        @Binding var incrementMe: Int
        var body: some View {
            Button("Button With Binding") {
                incrementMe += 1
            }
        }
    }
    struct ContentView: View {
        @State private var content = Content()
    
        var body: some View {
            VStack {
                Text("tapCount: \(content.tapCount)")
                Tapper(incrementMe: $content.tapCount)
                    .padding(4.0)
                Button("Button Without Binding") {
                    content.increment()
                }
            }
            .padding()
        }
    }