Using the PreferenceKey Protocol to Align Views in SwiftUI
If you are used to laying out views using Interface Builder, then it might feel like somebody stole your toolbox when you’re in SwiftUI
You’ve got a simple view consisting of three
TextField items, and you’ve written your code to look like this:
When you run it, this is what you get:
Ugh, you think. But then you remember reading somewhere about
Spacer(), so you try adding it between the
Guess what? Neither of those make any difference.
So then you hunt around the internet for a solution. Nothing seems to work. Then, a friend asks if you’ve watched the WWDC 19 video on SwiftUI essentials. You haven’t, so you sit through an hour of two guys extolling the virtues of SwiftUI. You get really excited (again). But you miss the few seconds around 52 minutes in which preferences are given a passing mention.
Preferences are clever things — especially the
PreferenceKey protocol. Here’s what it looks like:
In the rest of this article, I’ll show you how to use this to obtain a result that looks like this:
The definition, according to the GitHub file, says: A
PreferenceKey is a named value produced by a view. Views with multiple children automatically combine all child values into a single value visible to their ancestors.
ColumnWidthPreferenceKey below is my implementation of
The values collected from the
Text views are their widths, and these are formed into an array of
ColumnWidthPreference values by the
So how does this happen?
Here is the code that generates the view in Figure 2.
As you can see, it’s a
Form consisting of three
HStacks, each containing a
Text view followed by a
TextField view. Each of these accepts a value into a
Binding. The important features — as far as this article is concerned — are:
The argument for
columnWidthEqualiserView, defined as:
Here, we have a declaration of a
GeometryReader, which is defined to be a container view that defines its content as a function of its own size and coordinate space.
The way it does this is via a
GeometryProxy, which provides access to the size and coordinate space of the container view.
In the example code, the container view is the
Text view. The
GeometryReader is followed by a closure whose single argument is just such a
GeometryProxy. What the closure does is create a
Rectangle shape inside the container modified by
fill to fill the view with a clear colour and
preference to generate a key/value pair for
ColumnWidthPreferenceKey. This is how it builds an array of width values for the
Form modifier’s argument is
ColumnWidth, defined as:
What this does is respond to a change to the
It then iterates through the preference key/value pairs for all the child views looking for the widest
Text view (+20), which it saves to
width the argument passed to it. The views are then rebuilt using the new width, which is where
frame comes in.
OK, so this is a bit convoluted — but it works.