SwiftUI equal and ideal sizes
One of the more common sentiments towards SwiftUI you’ll come across on the Internet goes something like this: It’s magical until it isn’t. Meaning: SwiftUI let’s you write UI code so quickly that it sometimes feels like a superpower. But suddenly you hit a wall and there’s seemingly no way around it. One example that illustrates this quite well is when you want to make all children of a VStack
as wide as the widest child. Sounds like it should be straightforward. But it’s not. It’s a surprisingly hard problem.
Let’s stack up all of our favorite movies so we can pick our most favorite among them:
struct EqualWidthButtonStack: View {
let buttonTitles: [String] = [
"The Fast and the Furious",
"2 Fast 2 Furious",
"The Fast and the Furious: Tokyo Drift",
"Fast & Furious",
"Fast Five",
"Fast & Furious 6",
"Furious 7",
"The Fate of the Furious",
"F9"
]
var body: some View {
VStack {
ForEach(buttonTitles, id: \.self) { t in
Button(action: {}, label: {
Text(t)
})
.buttonStyle(.borderedProminent)
}
}
}
}
What we see, apart from the fact that it’s a very sad top movies list, is that each movie button is as wide as it needs to be to fit the title. Yet we want all of the buttons to have equal widths so our brain isn’t tricked to pick the one which stands out the most (Tokyo Drift!!!).
To achieve this, we’ll need to make two changes to our above code. First, we’ll wrap each Button’s Text
label in a .frame
modifier with maxWidth: .infnity
. That alone only solves our problem if we want our stack of buttons to occupy the full width it was given. If we want the stack of buttons to be as wide as it needs to be, we’ll need to make a second adjustment: apply the fixedSize
modifier to the VStack
.
struct EqualWidthButtonStack: View {
let buttonTitles: [String] = [
"The Fast and the Furious",
"2 Fast 2 Furious",
"The Fast and the Furious: Tokyo Drift",
"Fast & Furious",
"Fast Five",
"Fast & Furious 6",
"Furious 7",
"The Fate of the Furious",
"F9"
]
var body: some View {
VStack {
ForEach(buttonTitles, id: \.self) { t in
Button(action: {}, label: {
Text(t).frame(maxWidth: .infinity)
})
.buttonStyle(.borderedProminent)
}
}.fixedSize()
}
}
This yields the desired result:
There are, of course, a few other ways to achieve the same result. John Sundell has a great article with details.
How does it work
I’m probably the wrong person to attempt an explanation how exactly SwfitUI’s layout engine works under the hood. I didn’t come up with this solution myself. I was taught this technique by Paul Hudson in one of his workshops and it kind of blew my mind, because up until then I used the preference system to achieve something like that.
The key for this technique to work is that even though we wrap all the labels in a frame(maxWidth: .infinity)
modifier the idealWidth
of the Text
is still propagated to the parent VStack
. The VStack
then determines its own idealWidth
by looking at the idealWidth
of all children and picking the widest one. Applying the fixedSize
modfifier fixes the stack’s width at its ideal width (i.e. the width of the widest child).