Skip to main content

How to create an adaptive layout with ViewThatFits

About 4 minSwiftSwiftUIArticle(s)bloghackingwithswift.comcrashcourseswiftswiftuixcodeappstore

How to create an adaptive layout with ViewThatFits 관련

SwiftUI by Example

Back to Home

How to create an adaptive layout with ViewThatFits | SwiftUI by Example

How to create an adaptive layout with ViewThatFits

Updated for Xcode 15

New in iOS 16

SwiftUI gives us ViewThatFits so that we can have it select from one of several possible layouts based on what fits into the available screen space. This makes it a fantastic way to make sure your app looks great from the largest tvOS screen down to the smallest Apple Watch.

In its simplest form, you should list all the layout alternatives you want from most preferred to least preferred, and SwiftUI will try them all until it finds one that fits into the space:

ViewThatFits {
    Label("Welcome to AwesomeApp", systemImage: "bolt.shield")
        .font(.largeTitle)

    Label("Welcome", systemImage: "bolt.shield")
        .font(.largeTitle)

    Label("Welcome", systemImage: "bolt.shield")
}

Download this as an Xcode projectopen in new window

That attempts a long title in large text, a short title in large text, then finally a short title in small text – SwiftUI will try them in that order, and stop as soon as one fits into the available space.

This is particularly useful when you're working with views that can be arranged vertically or horizontally depending on space. For example, this creates a view with four different buttons, then decides to arrange them horizontally or vertically depending on how much space there is:

struct OptionsView: View {
    var body: some View {
        Button("Log in") { }
            .buttonStyle(.borderedProminent)

        Button("Create Account") { }
            .buttonStyle(.bordered)

        Button("Settings") { }
            .buttonStyle(.bordered)

        Spacer().frame(width: 50, height: 50)

        Button("Need Help?") { }
    }
}

struct ContentView: View {
    var body: some View {
        ViewThatFits {
            HStack(content: OptionsView.init)
            VStack(content: OptionsView.init)
        }
    }
}

Download this as an Xcode projectopen in new window

Where things get more interesting is how ViewThatFits handles handles text layout. Text in SwiftUI prefers to sit on one line, and by default ViewThatFits will prefer to avoid layouts the cause text to wrap. So, when space is limited code like this will default to a VStack rather than use a HStack with wrapping text:

ViewThatFits {
    HStack {
        Text("The rain")
        Text("in Spain")
        Text("falls mainly")
        Text("on the Spaniards")
    }

    VStack {
        Text("The rain")
        Text("in Spain")
        Text("falls mainly")
        Text("on the Spaniards")
    }
}
.font(.title)

Download this as an Xcode projectopen in new window

What's happening here is that ViewThatFits is measuring our text both horizontally and vertically, and is trying to find something that fits the text in both those dimensions – something where the text fits all on one line, without being truncated vertically.

This sometimes causes problems, but fortunately we can tell ViewThatFits to care about only one dimension so that we can get more control.

For example, say you wanted to display some terms and conditions to the user, and have it as fixed text if it can be fitted into the space, but scrolling text otherwise. This kind of code won't work as you expect:

struct ContentView: View {
    let terms = String(repeating: "abcde ", count: 100)

    var body: some View {
        ViewThatFits {
            Text(terms)

            ScrollView {
                Text(terms)
            }
        }
    }
}

Download this as an Xcode projectopen in new window

Unless you have a huge screen, that will always choose the scrolling version because we asked ViewThatFits to care about both horizontal and vertical axes for our text. This means as soon as the text runs across more than one line, SwiftUI will prefer to avoid that layout.

To fix this we need to restrict ViewThatFits to measure only the vertical axis, like this:

struct ContentView: View {
    let terms = String(repeating: "abcde ", count: 100)

    var body: some View {
        ViewThatFits(in: .vertical) {
            Text(terms)

            ScrollView {
                Text(terms)
            }
        }
    }
}

Download this as an Xcode projectopen in new window

Now that will allow the text to wrap horizontally, but as soon as it runs out of vertical space SwiftUI will move on to the ScrollView.

Similar solutions…
How to create a custom layout using the Layout protocol | SwiftUI by Example

How to create a custom layout using the Layout protocol
How to create a two-column or three-column layout with NavigationSplitView | SwiftUI by Example

How to create a two-column or three-column layout with NavigationSplitView
How to preview your layout in a navigation view | SwiftUI by Example

How to preview your layout in a navigation view
How to control layout priority using layoutPriority() | SwiftUI by Example

How to control layout priority using layoutPriority()
How to preview your layout in portrait or landscape | SwiftUI by Example

How to preview your layout in portrait or landscape

이찬희 (MarkiiimarK)
Never Stop Learning.