Skip to content

Instantly share code, notes, and snippets.

@Amzd
Last active September 20, 2023 05:27
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save Amzd/c3015c7e938076fc1e39319403c62950 to your computer and use it in GitHub Desktop.
Save Amzd/c3015c7e938076fc1e39319403c62950 to your computer and use it in GitHub Desktop.
SwiftUI Binding wrappers for willSet and didSet
extension Binding {
/// Wrapper to listen to didSet of Binding
func didSet(_ didSet: @escaping ((newValue: Value, oldValue: Value)) -> Void) -> Binding<Value> {
return .init(get: { self.wrappedValue }, set: { newValue in
let oldValue = self.wrappedValue
self.wrappedValue = newValue
didSet((newValue, oldValue))
})
}
/// Wrapper to listen to willSet of Binding
func willSet(_ willSet: @escaping ((newValue: Value, oldValue: Value)) -> Void) -> Binding<Value> {
return .init(get: { self.wrappedValue }, set: { newValue in
willSet((newValue, self.wrappedValue))
self.wrappedValue = newValue
})
}
}
struct FilterView: View {
@Binding var filters: [String]
@Binding var selectedFilters: Set<String>
var body: some View {
List(filters, id: \.self, selection: $selectedFilters.didSet {
print("Selection changed from \($1) to \($0)")
}, rowContent: { filter in
Text(filter)
})
}
}
@ugommirikwe
Copy link

ugommirikwe commented Apr 18, 2021

Hi there. Thanks for putting this out there, but I found a simpler code layout that can do this:

extension Binding {
   
    /// Extension method that provides a means to run functions in reaction to changes in the value of a `@Binding` property.
    /// - Parameter handler: The function to invoke when changes occur to the property's value.
    /// - Returns: A `Binding` object enhanced with a custom setter wherein the `handler` function is invoked.
    @discardableResult
    func onChange(_ handler: @escaping (Value) -> Void) -> Binding<Value> {
        Binding(
            get: { self.wrappedValue },
            set: { newValue in
                self.wrappedValue = newValue
                handler(newValue)
            }
        )
    }
}

You can then use the above like so:

struct FilterView: View {
    @Binding var filters: [String]
    @Binding var selectedFilters: Set<String>
    
    var body: some View {
        List(filters, id: \.self, selection: $selectedFilters.onChange {
            print("Selection changed to \($0)")
        }, rowContent: { filter in
            Text(filter)
        })
    }
}

Admittedly, this doesn't offer you access to the previous value in the setter like your code does.

@Amzd
Copy link
Author

Amzd commented Apr 18, 2021

Thanks for putting this out there, but I found a simpler code layout that can do this

You just renamed the function? xd

@discardableResult makes no sense because if you discard the new binding the handler won't get called..

@ugommirikwe
Copy link

ugommirikwe commented Apr 18, 2021

@discardableResult makes no sense because if you discard the new binding the handler won't get called..

@Amzd

Well, it'll be called. So, in the onAppear modifier for a SwiftUI widget I register the handler. Here's how I used it:

...someSwiftUIView
.onAppear {
    self.$error.onChange { newValue in
        someOtherProperty = newValue
    }
}

In any case, the @discardableResult doesn't mean the returned object won't be used, it simply means in case the call site doesn't assign it, then let Xcode know and stop worrying.

Does that make sense?

@Amzd
Copy link
Author

Amzd commented Apr 18, 2021

I don't think that works, you don't use the new returned Binding there? Am I missing how this works?

@ugommirikwe
Copy link

ugommirikwe commented Apr 18, 2021

So, look at the code again, it's very similar to your code--it returns a Binding<Value> which can be used, like in your example. But, in my case, with the .onAppear... hook, I just wanted to be able to execute an action whenever the @Binding variable's value changes. The error variable is already declared, like so:

@Binding var error: Error

I needed to (imperatively) update some other property whenever a new value is sent to the already-declared variable. So, that's why you see the .onAppear set up.

Meanwhile, here's where I got the code from: https://www.hackingwithswift.com/quick-start/swiftui/how-to-run-some-code-when-state-changes-using-onchange

Also, like I mentioned previously, the @discardableResult annotation is only to inform Xcode (and the compiler) that it's fine if the returned value isn't assigned to some other symbol. It doesn't actually prevent the extension function from returning the value. See: https://www.avanderlee.com/swift/discardableresult/ and https://www.hackingwithswift.com/example-code/language/how-to-ignore-return-values-using-discardableresult

You may test it and see for yourself.

@Amzd
Copy link
Author

Amzd commented Apr 20, 2021

I know what @discardableResult does. I don't really understand how you think your code would work, like how does the original binding know about your handler closure?..

I tested it because you were very confident, it doesn't work.

@ugommirikwe
Copy link

@Amzd my apologies. Yeah, using the extension function in the .onAppear() hook doesn't work, true. But it still works as in the original use case you provided: ...selection: $selectedFilters.onChange... In any case, the original intention was to offer a more concise version of the code as the one you created.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment