Skip to content

Instantly share code, notes, and snippets.

@perlguy99
Created December 9, 2019 16:37
Show Gist options
  • Save perlguy99/e21992706d7b25a3d181577889032302 to your computer and use it in GitHub Desktop.
Save perlguy99/e21992706d7b25a3d181577889032302 to your computer and use it in GitHub Desktop.
Custom Bindings (for property wrappers)

Custom Bindings

Thanks to Paul Hudson

This won't work because the @State property wrapper doesn't actually modify the value directly...

    @State private var blurAmount: CGFloat = 0 {
        didSet {
            print("New value is \(blurAmount)")
        }
    }

    var body: some View {
        VStack {
            Text("Hello, World!")
                .blur(radius: blurAmount)

            Slider(value: $blurAmount, in: 0...20)
        }
    }
}

To fix this we need to create a custom binding – we need to use the Binding struct directly, which allows us to provide our own code to run when the value is read or written.

In our code, we want a Binding to return the value of blurAmount when it’s read, but when it’s written we want to change the value of blurAmount and also print that new value so we can see that it changed. Regardless of whether we’re reading or writing, we’re talking about something that reads our blurAmount property, and Swift doesn’t allow us to create properties that read other properties because the property we’re trying to read might not have been created yet.

So, putting all that together we need to create a custom Binding struct that acts as a passthrough around blurAmount, but when we’re setting the value we also want to print a message. It’s also a requirement that we don’t store it as a property of our view, because reading one property from another isn’t allowed.

As a result, we need to put this code into the body property of our view, like this:

struct ContentView: View {
    @State private var blurAmount: CGFloat = 0

    var body: some View {
        let blur = Binding<CGFloat>(
            get: {
                self.blurAmount
            },
            set: {
                self.blurAmount = $0
                print("New value is \(self.blurAmount)")
            }
        )

        return VStack {
            Text("Hello, World!")
                .blur(radius: blurAmount)

            Slider(value: blur, in: 0...20)
        }
    }
}

Notice how some other things have stayed the same: we still use @State private var to declare the blurAmount property, and we still use blur(radius: blurRadius) as the modifier for our text view.

One thing that changed is the way we declare the binding in the slider: rather than using $blurAmount we can just use blur. This is because using the dollar sign is what gets us the two-way binding from some state, but now that we’ve created the binding directly we no longer need it.

OK, now let’s look at the binding itself. As you should be able to figure out from the way we used it, the basic initializer for a Binding looks like this:

init(get: @escaping () -> Value, set: @escaping (Value) -> Void)

You can find that by press Cmd+Shift+O and looking up “Binding” in the generated interface for SwiftUI. Breaking that down, it’s telling us that the initializer takes two closures: a getter that takes no parameters and returns a value, and a setter that takes a value and returns nothing. Binding uses generics, so that Value is really a placeholder for whatever we’re storing inside – a CGFloat in the case of our blur binding. Both the get and set closures are marked as @escaping, meaning that the Binding struct stores them for use later on.

What all this means is that you can do whatever you want inside these closures: you can call methods, run an algorithm to figure out the correct value to use, or even just use random values – it doesn’t matter, as long as you return a value from get. So, if you want to make sure you update UserDefaults every time a value is changed, the set closure of a Binding is perfect.

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