[SC]()

iOS. Apple. Indies. Plus Things.

Fun Alignment Guide Tricks

// Written by Jordan Morgan // Feb 9th, 2024 // Read it in about 3 minutes // RE: SwiftUI

This post is brought to you by Emerge Tools, the best way to build on mobile.

Setting your own vertical or horizontal alignment guide isn’t something I’ve thought about much when writing my own SwiftUI code. When they were announced, and later demo’d during a dub dub session in SwiftUI’s early days, I remember thinking, “Yeah, I don’t get that. Will check out later.”

Lately, though, I’ve seen two novel use cases where using one is exactly what was required. Or, at the very least, it solved a problem in a manageable way.

Rather than write out a bunch of code myself, I’ll get straight to it and show you the examples from other talented developers.

Creating the “Bottom Sheet”

Life, death and creating our own bottom sheet implementation, right? Sheet presentations have become more doable with the nascent iOS 16.4 APIs, allowing developers to set backgrounds and a corner radius on them. But, every now and then, you just have some weirdo requirement that necessitates rolling up your sleeves and doing it yourself.

The first inclination I’ve often seen is to use some sort of ZStack, .padding or .offset incantation:

@State private var bottomSheetOffset: CGFloat = 0.0

GeometryReader { geometry in
    VStack {
        Text("Sheet Content")
    }
    .frame(width: geometry.size.width, 
          height: geometry.size.height, 
          alignment: .top)
    .clipShape(
        UnevenRoundedRectangle(topLeadingRadius: 32, 
                               topTrailingRadius: 32, 
                               style: .continuous)
    )
    .frame(height: geometry.size.height, alignment: .bottom)
    .offset(y: bottomSheetOffset)
}

Or, maybe some sort of .transition:

@State private var showBottomSheet: Bool = false 

NavigationStack {
    // Content
    if showBottomSheet {
        VStack {
            Spacer()
            VStack(spacing: 18) {
                // Sheet content                
            }
            .padding([.leading, .trailing, .bottom])
            .background(.thickMaterial)
            .clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
            .padding()
        }
        .safeAreaPadding([.bottom], 40)
        .transition(.move(edge: .bottom).animation(.easeInOut(duration: 0.3)))
    }
}

Me? I’ve done the ZStack route before:

VStack {
    // Content
}
.overlay {
    ZStack {
        // Background dim
        Color.black
            .opacity(0.15)
            .onTapGesture {
                dimissOnCurtainTap()
            }
        // Container to easily push the UI down
        VStack {
            Spacer()
            // The UI
            VStack {
                // Sheet content
            }
            .padding(8)
            .frame(minWidth: 0, maxWidth: .infinity)
            .frame(height: containerHeight)
            .background {
                RoundedRectangle(cornerRadius: theme.screenCornerRadius)
                    .fill(Color(uiColor: .systemBackground))
                    .padding(EdgeInsets(top: 0,
                                        leading: contentPadding,
                                        bottom: contentPadding,
                                        trailing: contentPadding))
            }
            .offset(y: containerYOffset)
        }
    }
    .ignoresSafeArea()
    .opacity(internalIsPresenting ? 1 : 0)
    .allowsHitTesting(internalIsPresenting)

They all mostly work (to varying degrees), but here’s an approach from Ian Keen I liked using an alignment guide:

VStack {
    // Content
}
.overlay(alignment: .bottom) {
   Color.white.frame(height: 50)
      .alignmentGuide(.top) { $0[.bottom] }
}

That’s an abridged version, you’d still need to hook in the offset to show it, but the idea is that to actually place the bottom sheet — you use .alignmentGuide(.top) { $0[.bottom] }. This basically says “align this content’s top origin to the parent’s bottom origin”, which puts the bottom sheet in the right spot to later present.

Smooth Animations

Ben Scheirman had a great example showing how alignment guides can give you the animation you could be after. I’d encourage you to read his post on the matter, though the gist is that by using alignment guides — he can get two rectangles in a ZStack to animate left and right smoothly from the center (they begin one on top of the other):

@State private var isLinked: Bool = false 

ZStack {
     BarView()
         .alignmentGuide(HorizontalAlignment.center) { d in
             d[isLinked ? .center : .trailing]
         }
     BarView()
         .alignmentGuide(HorizontalAlignment.center) { d in
             d[isLinked ? .center : .leading]
         }
 }

The result is that they “split” from the center, evenly. Without using alignment guides for this particular scenario, SwiftUI’s layout system can have some unintended effects on the resulting animation. His post shows this clearly with some pictures, go check it out.

If you want to dig in a bit deeper over how alignment guides work, I’d recommend reading these posts:

  • Paul Hudson has a great, overall explainer (as he always does).
  • SwiftUI Lab has an insane, in-depth post on the matter (and, as he always does).

When I don’t really “get” an API, I find the only way I learn it is by getting to the point where I can answer this question:

When would this API help me? Would I know when to reach for it?

After seeing these examples and then going back to the docs, I feel like I’m getting there with alignment guides.

Until next time ✌️.

···

Spot an issue, anything to add?

Reach Out.