iosswiftswiftui

Pass a binding through a view preference in SwiftUI


I'm trying to pass value changes of a binding into a view modifier. Similar to some of the other view modifiers that are being used inside Apple, e.g.;

func presentationDetents(
    _ detents: Set<PresentationDetent>,
    selection: Binding<PresentationDetent>
) -> some View

But the view modifier never gets called when the binding changes. Whenever I use a plain value it works.

Here is a single file example of what I'm trying to do and what works and what does not work marked.

import SwiftUI

extension View {
    func changeNumber(number: Int) -> some View {
        print(number) // When running changeNumber with a plain value this does get called.
        return self
    }
    
    func changeNumber(number: Binding<Int>) -> some View {
        print("Binding", number) // Does not get called
        return self
    }
}

struct DebugView: View {
    @State var number = 0
    
    var body: some View {
        VStack {
            Button("Change Number") {
                number = Int.random(in: 0..<50)
            }
            
            Text("Hello, World!")
                .changeNumber(number: $number) // $number breaks, number works.
        }
    }
}

struct DebugView_Previews: PreviewProvider {
    static var previews: some View {
        DebugView()
    }
}


Solution

  • Alright, with some pointers of @lorem ipsum, I now know what I did wrong. I solved it by using the wrapped value on the preference key and passing that up in the view chain. Final code that does work;

    import SwiftUI
    
    public struct NumberKey: PreferenceKey {
        public static var defaultValue: Int = 0
    
        public static func reduce(value: inout Int, nextValue: () -> Int) {
            value = nextValue()
        }
    }
    
    
    extension View {
        func changeNumber(number: Int) -> some View {
            print(number)
            return self
        }
        
        func changeNumber(number: Binding<Int>) -> some View {
            print("Binding", number.wrappedValue)
            return self.preference(key: NumberKey.self, value: number.wrappedValue)
        }
    }
    
    struct DebugView: View {
        @State var number = 0
        
        var body: some View {
            VStack {
                Button("Change Number") {
                    number = Int.random(in: 0..<50)
                }
                
                Text("Hello, World!")
                    .changeNumber(number: $number)
            }
            .onPreferenceChange(NumberKey.self) { value in
                print("Changed", value)
            }
        }
    }
    
    struct DebugView_Previews: PreviewProvider {
        static var previews: some View {
            DebugView()
        }
    }