Unfuddling the SwiftUI Alignment Guide API
In general I think Apple’s APIs are pretty good. However there are the occasional exceptions where my brain, for whatever reason, can’t seem to get a hold on the essence of the API design and results in me constantly looking up the docs. In UIKit, the UIAnimatedTransitioning
and UIPresentationController
APIs come to mind.
In SwiftUI, I’d say the most confusing (to me) is the alignment guide system.
I’m working on an update to one of my apps and I want to have a draggable bar (like a vertical slider) that can split into two when you tap on it.
This type of interface could be written like this:
struct DemoView: View {
@State var isLinked = true
var body: some View {
HStack {
BarView()
if !isLinked {
BarView()
}
}
.onTapGesture {
withAnimation {
isLinked.toggle()
}
}
}
}
Doing this doesn’t look great when you see the animation, however.
The reason for this is that the HStack
has aligned the left bar in the center, and then when it re-renders with 2 bars it slides the bar to the new position. The right bar however was never in the view hierarchy before, so it just fades in to the new position.
In an attempt to fix this, I reached for the .alignmentGuide
API, however no matter what I tried, modifying the HorizontalAlignment
properties had no effect.
I have probably tried this exact thing a dozen times and am frustrated every time it doesn’t work, hence this post. It’s as much for future me as it is for you, dear reader.
How Alignment Guides work
Using the excellent SwiftUI Companion app (must buy if you work with SwiftUI in my opinion) I was able to generate a playground for visualizing how this works:
The alignmentGuide
modifier takes the guide you want to modify (HorizontalAlignment
or VerticalAlignment
and then the closure lets you specify how you want to modify the view in relation to that.
So in the screenshot above, look at the middle bar. It’s .leading
edge (of the parent VStack
) is aligned with the .center
of the yellow bar. Note that these are all horizontal guides in a vertical layout.
This seems confusing because it doesn’t look like the leading edge is there. But if you play with the toggles here you’ll see what is happening. The edges of the VStack
are being pushed out because of the alignment guides. Without the last one enabled, it looks like this:
There are two key insights here.
- the alignment guide passed to the
.alignmentGuide(…)
method refers to the container, not the view we’re modifying. - the alignment guides influence the layout of the cross dimension of the stack. So for a
VStack
you can control the horizontal alignment (but clearly the bars here are still stacked vertically). For anHStack
you can control the vertical alignment.
So in my case I need a ZStack
so I can have them vertically aligned and horizontally offset from each other in a way I can modify with the alignment guide.
Implementing the ZStack solution
Changing our HStack
to a ZStack
means that the items will be laid on top of one another, centered in both dimensions (by default). We can change the horizontal alignment of each depending on the value of the isLinked
state property.
We also want to remove the if
condition in our body because this will cause the 2nd bar to be inserted and removed from the hierarchy as we tap the button. We don’t want this because we want to be in control of the animation.
ZStack {
BarView()
.alignmentGuide(HorizontalAlignment.center) { d in
d[isLinked ? .center : .trailing]
}
BarView()
.alignmentGuide(HorizontalAlignment.center) { d in
d[isLinked ? .center : .leading]
}
}
With this in place, our view is animating nicely.
My hope is that by writing this I will now remember this API, and hopefully you will too!