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.

one vertical bar two vertical bars

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:

Screenshot 2024-01-20 at 11.40.58 AM.png

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:

Image.png

There are two key insights here.

  1. the alignment guide passed to the .alignmentGuide(…) method refers to the container, not the view we’re modifying.
  2. 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 an HStack 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!