Reading time: 1 min

This recipe shows how to implement responsive, adaptive SwiftUI layouts using ViewThatFits. This view adapts to the available space by providing the first child view that fits.

The end result looks like this:

preview

ViewThatFits is available starting in SwiftUI 4 (iOS 16, macOS 12.4).

Responsive views change based on the available space in their superviews. Consider a custom progress indicator: if you have enough space, you'd like to display a progress bar and a text view indicating the current progress. If there's less space available, you're fine with showing just the progress bar and if the layout is even more tight, just the text field suffices.

ViewThatFits will attempt to render each of its child views consecutively until it finds one that fits. This means that you'd normally put "larger" views before "smaller" ones:

struct CustomProgressView: View {
  @State var progress: Double

  var body: some View {
    // chooses based on available horizontal space
    ViewThatFits(in: .horizontal) { 
      HStack { // largest variant
        textView
        progressView
      }
      progressView
      textView // smallest variant
    }
    .padding(3)
    .background(Rectangle() // background to showcase view bounds
      .foregroundColor(.clear)
      .border(Color.blue, width: 2))
  }

  private var textView: some View {
    Text("\(progress.formatted(.percent))")
  }

  private var progressView: some View {
    ProgressView(value: progress)
      .frame(width: 100)
  }
}

And here's a showcase setup, in which all three variants of CustomProgressView are shown by manually constraining them with frame:

VStack {
  CustomProgressView(uploadProgress: 0.75)
    .frame(maxWidth: 200)
  CustomProgressView(uploadProgress: 0.75)
    .frame(maxWidth: 110)
  CustomProgressView(uploadProgress: 0.75)
    .frame(maxWidth: 50)
}

Next Post Previous Post