SwiftUI View That Fits

The SwiftUI ViewThatFits view, introduced in iOS 16, makes it a lot simpler to build layouts that adapt to make best use of the available space.

Adaptive Layouts

I’ve written a few times about building adaptive layouts with UIKit and SwiftUI. For an example see SwiftUI adaptive stack views. What those solutions have in common is the need to find breakpoint conditions when the layout should change.

A common example is switching between horizontal and vertical stack layouts for regular or compact horizontal size classes:

if horizontalSizeClass == .compact {
  VStack { content }
} else {
  HStack { content }
}

In practise, I sometimes need multiple nested stack views and conditional views which I adapt for a combination of conditions. Those conditions can include the size class, dynamic type size, and when size class is too coarse, the view width which requires the use of a GeometryReader.

This approach needs careful experimentation to identify good breakpoints for different screen widths, content size, and dynamic type size. It’s easy to miss an edge case that either fails to make best use of the available space or worse has content spilling offscreen.

ViewThatFits

In iOS 16, Apple added the ViewThatFits view to SwiftUI that allows us to take a different approach. Instead of finding conditions for when the layout should change we pass a series of views, in order of preference, and let the SwiftUI layout system pick the first view that fits in the available space.

ViewThatFits(in: .horizontal) {
  WideView()
  RegularView()
  CompactView()
}

The ViewThatFits view takes a set of axes as a parameter that allow you to constrain the view to fit in the horizontal and/or vertical axes. The default, if you omit the axes, is to use both axes:

// default to both axes
ViewThatFits()

That’s the same as specifying both horizontal and vertical axes:

// Same as default, use both axes
ViewThatFits(in: [.horizontal, .vertical])

Note this is not the same as specifying an empty set for the axes which forces ViewThatFits to always use the first view:

// Probably not what you want?
ViewThatFits(in: []) 

In my example I have everything in a vertical scroll view so I only want to constrain to the available horizontal space.

A Practical Example

Here’s my example layout of a timer control shown when there is available horizontal space:

Horizontal layout with left button, timecode, right button, and red reset button

If there is insufficient horizontal space the layout should switch to a compact vertical layout. Note that the order of the views also changes:

Vertical layout with timecode above horizontal layout of left button, reset button, and right arrow button

To build this with ViewWithFit we can first build the two view variations. First the horizontal variation which places everything in a horizontal stack:

private struct StandardView: View {
  let timecode: String
    
  var body: some View {
    HStack(spacing: 16) {
      DownButton()
      TimerView(timecode: timecode)
      UpButton()
      ResetButton()
    }
  }
}

Then the compact view that embeds a horizontal stack in a vertical stack:

private struct CompactView: View {
  let timecode: String
    
  var body: some View {
    VStack(spacing: 8) {
      TimerView(timecode: timecode)
      HStack() {
        DownButton()
        ResetButton()
        UpButton()
      }
    }
  }
}

This ability to separate out and work on the view variations independently is what I think makes this approach easier to deal with. We build the final timer control view by passing both views to ViewThatFits. Note that I’ve placed the standard view before the compact view so that ViewThatFits will prefer it when it fits in the available space.

struct TimerControl: View {
  let timecode: String
    
  var body: some View {
    ViewThatFits(in: .horizontal) {
      StandardView(timecode: timecode)
      CompactView(timecode: timecode)
    }
  }
}

A Final Tip

One final tip when building adaptive layouts. Don’t forget about the device variants feature of Xcode 14 previews. It’s a quick way to preview a layout with variations of dynamic type and orientation:

Xcode preview with iPhone in portrait and landscape with AX 5 dynamic type size

Learn More