südrocket.de

May 22, 2022

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).

References