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.

Using ViewThatFits to replace GeometryReader 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.

How the view adapts its content based on the available width

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.

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

ViewThatFits in action when orientation changes

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.