Size-Limiting Frames

Occasionally we come across a layout where we need to limit the bounds of a view in a particular dimension. For example we might have a view where we show the character portrait along with the title they might be introduced by at court.

This works fine for the brawler:

character portrait with short title

But for the dragoon who has a bit of a khaleesi complex, it gets a little bit unwieldy and we need to limit the number of titles we actually show:

character portrait with long title

One way we can do this is with a .lineLimit on the Text, but when combined with accessibility font sizes, that can still grow taller than we intend. Sometimes we need to constrain a height in terms of pixels.

We might try to use a .frame with maxHeight specified:

struct ContentView : View {
    @ObservedObject var character: Character

    var body: some View {
        // ⚠️ This is an example that does not work.
        HStack {
            Image(character.imageName)
            Text(character.title)
                .frame(maxHeight: 200)
        }
    }
}

This works great for the dragoon’s overly long title, allowing it to flow on as many lines as can fit in the space, and then truncating the rest:

character portrait with truncated title

But when we try it on the brawler, there’s a new problem:

character portrait with short title and too-large frame

We specified a maximum height for the frame intending to limit the height of the text, which it does, but the frame itself has chosen the height we specified as the maximum, rather than the height of the text within it.

If we review flexible frames we can understand why.

When we omit a constraint to .frame, the frame is layout-neutral for that constraint and chooses the size chosen by its child. But when we provide a value the frame is no longer layout-neutral for that constraint, and no longer considers the size chosen by the child.

The frame received a proposed size from its parent, and applied the maximum height constraint, proposing the maximum height to the Text child, which in the dragoon’s case resulted in the truncation of their overly long title.

In both cases the Text chose a size equal or smaller to that height. Since the frame was layout-neutral in terms of minimum height, this set the minimum height of the frame to the size of the child, but the maximum height was supplied by us. The size proposed by the frame’s parent was outside the range of these two heights, and was constrained by the frame: to its maximum height.

This wasn’t what we wanted, we wanted the frame to constrain the size of the child, but still be layout-neutral.

Fortunately there’s a solution for this, and it involves the third set of .frame parameters we didn’t consider yet, the ideal size. Since we didn’t specify any value for .idealHeight then the frame’s ideal height is layout-neutral, that is, the ideal height of the frame is the height of the child.

SwiftUI gives us modifiers that fix the size of a view to its ideal size:

/// Fixes this view at its ideal size.
func fixedSize() -> some View

/// Fixes the view at its ideal size in the specified dimensions.
func fixedSize(horizontal: Bool, vertical: Bool) -> View

As we’re dealing with multi-line text we don’t want the first variant since text always ideally wants to be rendered on just one line, but the second variant is perfect since it will allow us to fix just the height of the frame.

We want to fix the size of the .frame so the modifier goes after it, rather then before—which would fix the size of the Text:

struct ContentView : View {
    @ObservedObject var character: Character

    var body: some View {
        HStack {
            Image(character.imageName)
            Text(character.title)
                .frame(maxHeight: 200)
                .fixedSize(horizontal: false, vertical: true)
        }
    }
}

The dragoon’s long title renders exactly as before, constrained by the maximum height of the frame:

character portrait with truncated title and correct-sized frame

But now the frame around the brawler’s title is fixed in height to the frame’s ideal size, that of the brawler’s title, and does not expand the stack unnecessarily:

character portrait with short title and correct-sized frame

Imagery used in previews by Kaiseto, original images and derived here licensed under Creative Commons 3.0 BY-NC-SA.