Using ViewThatFits to replace GeometryReader in SwiftUI
This article shows how to use the new ViewThatFits released at WWDC 2022 to replace GeometryReader when building views in SwiftUI.
In this article, I will describe how I used the brand new ViewThatFits
in my Basketball Coach app.
Before ViewThatFits
Until iOS 15 it was not easy to write a view that adapts its content based on the available space. Let's take a look at PlayerRow
, a view that displays a player's information, such as the number and full name.
This row is used in different contexts and, of course, on different devices and orientations. In order to adapt its content my approach was the following:
1. I wrapped the view in a GeometryReader
2. I used its GeometryProxy
's size to get the available width
3. I applied a little bit of math
var body: some View {
GeometryReader { proxy in
if proxy.size.width > 180 {
// display full row
} else if proxy.size.width > 100 {
// display row with short name
} else {
// display only the left component
}
}
}
This works. It is not the most declarative solution though. So let's have a look at how this works with a ViewThatFits.
ViewThatFits
With iOS 16 we can forget about calculations and let the view do all the job.
How does it work?
We simply need to give to the view a set of children that represent the fallbacks. ViewThatFits will pick the first child view that fits.
var body: some View {
ViewThatFits(in: .horizontal) {
// first child
FullRow(player: player)
// first fallback
FullRowWithShortName(player: player)
// second fallback
SmallRow(player: player)
}
}
Handling truncated text with ViewThatFits
There is a great article by nilcoalescing.com that describes this in details.
The default behavior for a Text
is to truncate if there is not enough available space. Unfortunately, this means that for ViewThatFits a truncated Text
is still an eligible child.
How do we instruct ViewThatFits to skip to the following view (if exists) when Text is truncated? We can explicitly set the maximum number of lines that text can occupy and force the full horizontal size of the text using the fixedSize()
modifier:
Text(player.fullName)
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
In general, we have to use fixedSize()
carefully because this way the view can exceed its parent's bounds, which might be unexpected behavior.
When we add such a view as a child of ViewThatFits though, the parent will discard it when it exceeds the available space and select the next one that fits (if exists).
Just like that! When there is not enough space to display the player's full name the view chooses the first fallback that fits, in this case, the one displaying the short name.