How do alignment guides and custom alignments work, and why does moving one view’s guide move its siblings?
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
offsetto 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… →