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:
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:
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:
But when we try it on the brawler, there’s a new problem:
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:
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: