SwiftUI gives us two environment values to monitor the current size class of our app, which in practice means we can show one layout when space is restricted and another when space is plentiful.
For example, in our current layout we’re displaying the resort details and snow details in a HStack
, like this:
HStack {
ResortDetailsView(resort: resort)
SkiDetailsView(resort: resort)
}
Each of those subviews are internally using a Group
that doesn’t add any of its own layout, so we end up with all four pieces of text laid out horizontally. This looks great when we have enough space, but when space is limited it would be helpful to switch to a 2x2 grid layout.
To make this happen we could create copies of ResortDetailsView
and SkiDetailsView
that handle the alternative layout, but a much smarter solution is to have both those views remain layout neutral – to have them automatically adapt to being placed in a HStack
or VStack
depending on the parent that places them.
First, add this new @Environment
property to ResortView
:
@Environment(\.horizontalSizeClass) var horizontalSizeClass
That will tell us whether we have a regular or compact size class. Very roughly:
Things get a little more complex for iPad when it comes to split view mode, which is when you have two apps running side by side – iOS will automatically downgrade our app to a compact size class at various points depending on the exact iPad model.
Fortunately, to begin with all we care about are these two horizontal options: do we have lots of horizontal space (regular) or is space restricted (compact). If we have a regular amount of space, we’re going to keep the current HStack
approach so that everything its neatly on one line, but if space is restricted we’ll ditch that and place each of the views into a VStack
.
So, find the HStack
that contains ResortDetailsView
and SkiDetailsView
and replace it with this:
HStack {
if horizontalSizeClass == .compact {
VStack(spacing: 10) { ResortDetailsView(resort: resort) }
VStack(spacing: 10) { SkiDetailsView(resort: resort) }
} else {
ResortDetailsView(resort: resort)
SkiDetailsView(resort: resort)
}
}
.padding(.vertical)
.background(.primary.opacity(0.1))
As you can see, that uses two vertical stacks placed side by side, rather than just having all four views horizontal.
Is it perfect? Well, no. Sure, there’s a lot more space in compact layouts, which means the user can use larger Dynamic Type sizes without running out of space, but many users won’t have that problem because they’ll be using the default size or even smaller sizes.
To make this even better we can combine a check for the app’s current horizontal size class with a check for the user’s Dynamic Type setting so that we use the flat horizontal layout unless space really is tight – if the user has a compact size class and a larger Dynamic Type setting.
First add another property to read the current Dynamic Type setting:
@Environment(\.dynamicTypeSize) var dynamicTypeSize
Now modify the size class check to this:
if horizontalSizeClass == .compact && dynamicTypeSize > .large {
Now finally our layout should look great in both orientations: one single line of text in a regular size class, and two rows of vertical stacks in a compact size class when an increased font size is used. It took a little work, but we got there in the end!
Our solution didn’t result in code duplication, which is a huge win, but it also left our two child views in a better place – they are now there just to serve up their content without specifying a layout. So, parent views can dynamically switch between HStack
and VStack
whenever they want, and SwiftUI will take care of the layout for us.
Before we’re done, I want to show you one useful extra technique: you can limit the range of Dynamic Type sizes supported by a particular view. For example, you might have worked hard to support as wide a range of sizes as possible, but found that anything larger than the “extra extra extra large” setting just looks bad. In that situation you can use the dynamicTypeSize()
modifier on a view, like this:
.dynamicTypeSize(...DynamicTypeSize.xxxLarge)
That’s a one-sided range, meaning that any size up to and including .xxxLarge
is fine, but nothing larger. Obviously it’s best to avoid setting these limits where possible, but it’s not a problem if you use it judiciously – both TabView
and NavigationStack
, for example, limit the size of their text labels so the UI doesn’t break.
SPONSORED Alex is the iOS & Mac developer’s ultimate AI assistant. It integrates with Xcode, offering a best-in-class Swift coding agent. Generate modern SwiftUI from images. Fast-apply suggestions from Claude 3.5 Sonnet, o3-mini, and DeepSeek R1. Autofix Swift 6 errors and warnings. And so much more. Start your 7-day free trial today!
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.