How do ViewThatFits and AnyLayout differ, and which one preserves state when the arrangement changes?
Predict first
What re-appears after the switch, and which child does ViewThatFits choose?
@Observable final class Driver { var wide = true }
let driver = Driver()
nonisolated(unsafe) var tokens = 0
final class Token { let id: Int; init() { tokens += 1; id = tokens } }
struct Item: View {
let name: String
@State private var token = Token()
var body: some View { Color.gray.frame(width: 50, height: 20).onAppear { note("\(name) appears (token \(token.id))") } }
}
struct WithAnyLayout: View {
var body: some View {
let layout = driver.wide ? AnyLayout(HStackLayout()) : AnyLayout(VStackLayout())
layout { Item(name: "p"); Item(name: "q") }
}
}
struct WithBranch: View {
var body: some View {
if driver.wide { HStack { Item(name: "p"); Item(name: "q") } }
else { VStack { Item(name: "p"); Item(name: "q") } }
}
}
_ = show(WithAnyLayout()); log = []
driver.wide = false; spin()
print("AnyLayout:", log.joined(separator: ", ")); log = []
tokens = 0; driver.wide = true
_ = show(WithBranch()); log = []
driver.wide = false; spin()
print("if/else:", log.joined(separator: ", ")); log = []
struct Pick: View {
let w: CGFloat
var body: some View {
ViewThatFits {
Color.red.frame(width: 250, height: 10).onAppear { note("250") }
Color.green.frame(width: 150, height: 10).onAppear { note("150") }
Color.blue.frame(width: 60, height: 10).onAppear { note("60") }
}.frame(width: w, height: 40)
}
}
for w in [300.0, 200.0, 100.0, 30.0] { _ = show(Pick(w: w)); print("width \(Int(w)) picks", log.joined()); log = [] }
- AnyLayout: nothing new / if/else: both items appear again with new tokens / picks 250, 150, 60, 60
- AnyLayout re-creates the items
- ViewThatFits picks the first child always
Mental model. AnyLayout rearranges the same people around a table; if/else sends them home and invites a new group.
ViewThatFits picks the first child whose ideal size fits the proposal along the constrained axes, falling back to the last. In the check, in 300 points it picks the 250-wide child, in 200 the 150-wide one, and in 100 and in 30 the 60-wide one (the last child is the fallback even when it does not fit). It is for different designs of the same content (an icon row, then a two-line version, then a compact one); the children are separate views, so only the chosen one is in the tree and the others have no state.
AnyLayout switches the layout algorithm of the same children, for example HStackLayout to VStackLayout by size class, with an animation. Because the children keep their identity, nothing is re-created: after the switch no new onAppear fired and the tokens stayed 1 and 2. The equivalent if wide { HStack } else { VStack } is a different branch, so the children are destroyed and created again (they appeared again with tokens 3 and 4 in the check, after the first render had used 1 and 2) and any state, focus and scroll position is lost. Use AnyLayout (or Layout) for rearranging, ViewThatFits for substituting, and an if only when losing state is acceptable.
AnyLayout keeps identity; a branch change does not.
Common traps
- Switching stacks with
ifand wondering why text fields reset. The branch is a new identity. - Expecting
ViewThatFitsto consider height unless it is constrained. It only tests the axes you constrain.
They might follow up with
- How do you animate the change? Wrap the state change in
withAnimationwithAnyLayout. - What does
ViewThatFits(in: .horizontal)do? Test fit on the horizontal axis only.
Related questions
- 8.1.2 What is structural versus explicit identity in SwiftUI, and what happens to state when identity changes?
- 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 do alignment guides and custom alignments work, and why does… How do GridItem sizes (fixed, flexible, adaptive) and Grid differ in… →