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

Keith Lander
Oct 21 · 3 min read
Photo by Susan Holt Simpson on Unsplash

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:

Figure 1

Ugh, you think. But then you remember reading somewhere about Spacer(), so you try adding it between the Text and TextField items.

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:

Figure 2

The definition, according to the GitHub file, says: APreferenceKey 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 PreferenceKey:

The values collected from the Text views are their widths, and these are formed into an array of ColumnWidthPreference values by the reduce function.

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 Text modifiers: frame and background.
  • The Form modifier: modifier.

The argument for background is 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 Text views.

The Form modifier’s argument is ColumnWidth, defined as:

What this does is respond to a change to the ColumnWidthPreferenceKey.

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.

Better Programming

Advice for programmers.

Keith Lander

Written by

I am a published poet and author of WRITING SHED, an IOS & Mac app designed especially for writers.

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade