SwiftUI read preference helper
SwiftUI’s layout system is very nice to get started with. Combining stacks and modifiers you can get very far very quickly, but at some point you will need to pass information up the view hierarchy. Thankfully SwiftUI has a solution for that: preferences.
To make a preference you need to create a type that conforms to PreferenceKey
. That can sometimes get repetitive but today I don’t want to solve that part. The bit that bothers me more is what comes next. With that new type written you then start a dance of boilerplate that gets boring very quickly. Let’s explore how we can make our lives a little bit easier.
The first thing you need to do is make the child view write to the preference.
childView
.preference(key: MyPreferenceKey.self, value: value) }
This will make value available to the parent views. Next step is reading that, and it’s where the boilerplate gets too much repetitive for me.
@State private var childValue: String? = nil
var body: some View {
VStack {
Text(childValue ?? "")
MakeChildView()
.onPreferenceChange(MyPreferenceKey.self) { value in
childValue = value
}
}
}
Declare a @State
property to store the value read from the preference, use a onPreferenceChange
modifier that needs the type and a closure. A closure that it’s usually doing the same thing: storing the read value into the state property.
This API is very flexible. If you are doing something different on that closure every time it makes sense to use it, but most of the time we just need to mutate some local state to trigger a redraw. In that case, the API turns out to be too flexible.
So… what if we could simplify this a bit?
Reducing this boilerplate to the core of what we need is really easy. We just need an alternative method that reads the preference and stores it directly into the property that we want. Thankfully doing that is very easy.
extension View {
func readPreference<K>(
_ key: K.Type = K.self,
to binding: Binding<K.Value>
) -> some View where K : PreferenceKey, K.Value : Equatable {
onPreferenceChange(key) { value in
binding.wrappedValue = value
}
}
}
With this we can rewrite the previous code to get rid of the closure.
@State private var childValue: String? = nil
var body: some View {
VStack {
Text(childValue ?? "")
MakeChildView()
.readPreference(MyPreferenceKey.self, to: $childValue)
}
}
The new modifier reads very nicely and does its job without asking us for custom logic every single time. Note how the method accepts a binding, which means we could use other things to store the preference other than a state property.
Conclusion
As you can see, there is nothing revolutionary in this idea. Is just a simple method extension that implements a default behaviour. But that’s the whole point that I wanted to transmit. Sometimes we get stuck into repeating the same boilerplate over and over without realising that it’s very easy to get rid of it. Specially when the code is well architectured and the language provides you with the right tools.
This is not the only place where a small amount of boilerplate shows up when writing SwiftUI. With this post I want to show how reducing it is not very hard thanks to the architecture and the language. So keep an eye open and build your own library of little modifiers that get rid of the boring parts!