How do alignment guides and custom alignments work, and why does moving one view’s guide move its siblings?

8.12.2 · L5 · Interview

Predict first

Where do the two children end up?

extension HorizontalAlignment {
    enum Mid: AlignmentID { static func defaultValue(in d: ViewDimensions) -> CGFloat { d[HorizontalAlignment.center] } }
    static let mid = HorizontalAlignment(Mid.self)
}
@MainActor func run(_ label: String, _ v: some View) {
    frames = [:]
    _ = show(Root(content: v.frame(width: 300, height: 300, alignment: .topLeading)))
    print(label, f("a"), "|", f("b"))
}
run("default leading:", VStack(alignment: .leading, spacing: 0) {
    Color.red.frame(width: 40, height: 10).measure("a")
    Color.blue.frame(width: 80, height: 10).measure("b")
})
run("b leading = trailing:", VStack(alignment: .leading, spacing: 0) {
    Color.red.frame(width: 40, height: 10).measure("a")
    Color.blue.frame(width: 80, height: 10).alignmentGuide(.leading) { $0[.trailing] }.measure("b")
})
run("custom .mid guides:", VStack(alignment: .mid, spacing: 0) {
    Color.red.frame(width: 100, height: 10).alignmentGuide(.mid) { $0[.leading] + 30 }.measure("a")
    Color.blue.frame(width: 60, height: 10).alignmentGuide(.mid) { $0[.leading] + 10 }.measure("b")
})
  • default 0/0, guide moves the red sibling to x 80, custom .mid puts b at x 20
  • only the blue child moves in the second case
  • both children stay at 0 in every case

Mental model. Guides are a dotted line each view draws on itself; the stack hangs all the views from one common string through those dots.

A stack with an alignment (.leading, .center, …) lines each child up at the position of that alignment's guide, a number each child reports: by default the child’s own leading edge, centre and so on. .alignmentGuide(.leading) { d in … } overrides that number for one child. The surprise is in the check: giving the 80-wide child the guide d[.trailing] does not shift only that child. The stack aligns all guides to one line, so the red 40-wide sibling ends up at x = 80 while the blue child sits at 0: the siblings moved, because the alignment line has to pass through every child’s guide, and the stack grows to fit.

A custom alignment (an AlignmentID with a defaultValue) lets unrelated views line up across containers: two views report a guide for .mid (30 into the first, 10 into the second) and the stack puts those points on one line, which is why the second view lands at x = 20. Use it for form labels and values, a timeline spine, or a badge pinned to a text baseline in a different branch of the tree. Prefer guides to offset (which moves only pixels and leaves layout alone) and to GeometryReader measurement; use the firstTextBaseline and lastTextBaseline guides to align mixed font sizes.

The container lines all guides up on one line.

Common traps

  • Expecting a guide to move only one view. The container realigns everything to the common line.
  • Using offset to align. The layout (and neighbours’ positions) does not change.

They might follow up with

  • How do you align across separate stacks? A custom alignment shared by both containers, or a Layout.
  • What is the default guide for .center? The middle of the view in that axis.

Related questions

  • 8.5.1 How does SwiftUI’s layout negotiation work, and why do frame and padding order change the result?
  • 8.5.3 How does the Layout protocol work, and when is a custom layout the right tool?
  • 8.12.1 How does an HStack divide a width that is too small, and what do layoutPriority and fixedSize change?

← How does an HStack divide a width that is too small, and what do…   How do ViewThatFits and AnyLayout differ, and which one preserves… →