How does the Layout protocol work, and when is a custom layout the right tool?
Predict first
What size does this diagonal layout (children placed one after another, down and to the right) report?
import SwiftUI
func size<V: View>(_ v: sending V, _ w: CGFloat = 400, _ h: CGFloat = 400) -> CGSize {
UIHostingController(rootView: v).sizeThatFits(in: CGSize(width: w, height: h))
}
struct Diag: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
var w: CGFloat = 0, h: CGFloat = 0
for s in subviews { let z = s.sizeThatFits(.unspecified); w += z.width; h += z.height }
return CGSize(width: w, height: h)
}
func placeSubviews(in b: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
var p = b.origin
for s in subviews {
s.place(at: p, proposal: .unspecified)
let z = s.sizeThatFits(.unspecified)
p.x += z.width; p.y += z.height
}
}
}
print(size(Diag { Color.red.frame(width: 10, height: 10); Color.blue.frame(width: 20, height: 5) }))
- (30, 15)
- (20, 10)
- (10, 5)
Mental model. A custom layout is writing your own rules for the conversation: you ask each child how big it would like to be, then you decide where each stands.
A type conforming to Layout takes over the negotiation for its subviews. You implement two required methods. sizeThatFits(proposal:subviews:cache:) returns the size the container wants given a ProposedViewSize (whose width and height are optional: nil means “ideal”, zero means “minimum”, infinity means “maximum”). placeSubviews(in:proposal:subviews:cache:) positions each subview by calling place(at:anchor:proposal:) with the proposal you want to give it. Subview proxies let you ask a child its size for any proposal (subview.sizeThatFits(.unspecified)), its spacing preferences and its layoutValue (set by a child through a LayoutValueKey). The optional cache avoids repeating expensive measurement between the two calls, and you can add explicitAlignment to define alignment guides.
You use the layout like a stack: MyLayout { child1; child2 }. It is the right tool for arrangements stacks cannot express: flow or wrapping layouts, radial menus, tag clouds, equal-width columns, masonry. Prefer built-ins (Grid, LazyVGrid, ViewThatFits) when they fit. It is also the correct replacement for many GeometryReader hacks, because it participates in the real negotiation, so animation and sizing stay correct. Wrap it in AnyLayout to switch between layouts with an animation. A custom layout is not lazy: it measures all its subviews, so use it for modest numbers of children, not unbounded lists.
The layout adds up the children’s ideal sizes: 10 + 20 wide and 10 + 5 tall. Its own sizeThatFits decides the container size.
Common traps
- Ignoring the proposal and returning a fixed size. The layout then does not respond to its container.
- Using it for thousands of children. It measures them all; use lazy containers for large data.
They might follow up with
- How do you animate between two layouts? Wrap them in
AnyLayoutand change which one it holds. - How does a child pass data to the layout? A
LayoutValueKeyread through the subview proxy.
Related questions
- 8.5.1 How does SwiftUI’s layout negotiation work, and why do frame and padding order change the result?
- 8.5.2 When do you use GeometryReader, and how do safe areas work in SwiftUI?
- 8.7.1 What is the difference between implicit and explicit animation, and how do transitions fit in?
← When do you use GeometryReader, and how do safe areas work in SwiftUI?