How to create an adaptive layout with ViewThatFits
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")
}
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)
}
}
}
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)
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)
}
}
}
}
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)
}
}
}
}
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
.