- Published on
- 7 min read Intermediate
> Building Fold-Aware Layouts with ArrangementView in SwiftUI
Plenty of screens are really two things at once. A recipe and its ingredient list. A map and the card for the place you tapped. A video and its playback controls. On a regular iPhone you'd put them in an HStack, a VStack, or a ZStack and be done with it, but on iPhone Duo those stacks don't know about the fold. Partially fold the inner display and your carefully centered divider ends up right on the crease.
ArrangementView is the iOS 27.1 answer. You give it a primary view and a secondary view, and it works out a layout from the available size, the size class, and hardware features like the fold. When the environment changes, it rearranges the two views for you.
Split or Overlay
There are two built-in styles. The default, .automatic, resolves to a split arrangement, so the simplest version needs no configuration at all:
struct RecipeScreen: View {
let recipe: Recipe
var body: some View {
ArrangementView {
RecipeStepsView(recipe: recipe)
} secondary: {
IngredientsPanel(recipe: recipe)
}
}
}
A split arrangement puts the two views side by side when the container is wider than it is tall, and stacks the primary view above the secondary view when it's taller than it is wide. When the fold is active, it shifts the views so neither one lands on it.
An overlay arrangement layers the primary view on top of the secondary view instead. When iPhone Duo is closed or fully open, that's all it does. When the device is partially folded, the overlay pulls the two views apart so each gets its own side of the fold, with the primary view on the trailing or bottom side by default. Here's a map with a floating place card:
struct PlaceBrowser: View {
@State private var selectedPlace: Place?
var body: some View {
ArrangementView {
PlaceCard(place: selectedPlace)
.overlayArrangementEdge(.leading)
} secondary: {
PlacesMap(selection: $selectedPlace)
}
.arrangementViewStyle(.overlay)
}
}
overlayArrangementEdge(_:) picks the horizontal edge a view moves to when the overlay turns into a side-by-side layout. Here I want the card on the leading side and the map on the other half of the fold.
Apple's HIG has a handy rule for choosing between the two. If your current layout looks like an HStack or VStack, it maps to a split arrangement. If it looks like a ZStack, it maps to an overlay.
Restricting the Axes
Sometimes one of the two layouts doesn't make sense. Ingredients next to the steps is great on a wide display, but squeezing them underneath the steps on the outer display just makes both parts cramped. Both styles take an axes(_:) modifier:
ArrangementView {
RecipeStepsView(recipe: recipe)
} secondary: {
IngredientsPanel(recipe: recipe)
}
.arrangementViewStyle(.split.axes(.horizontal))
With only the horizontal axis allowed, the split still happens side by side when there's room, and in a tall container the arrangement shows just the primary view. That's why the primary slot should always hold the content that can stand on its own. On an overlay, axes(_:) limits which directions the overlay can spread into when it turns into a side-by-side layout.
Sizing the Split
By default the arrangement divides the space for you, but you can steer it. splitArrangementLayoutRatio(_:) sets a preferred fraction for one view. The overload with minimum, ideal, and maximum values lets you set different ratios for horizontal and vertical splits:
ArrangementView {
RecipeStepsView(recipe: recipe)
.splitArrangementLayoutRatio(
minHorizontal: 0.5,
idealHorizontal: 0.6,
maxHorizontal: 0.7,
idealVertical: 0.65
)
} secondary: {
IngredientsPanel(recipe: recipe)
}
.arrangementViewStyle(.split)
The view with the highest layoutPriority is sized first, and if the ratios leave part of the container unused, that same view fills the rest. If you'd rather think in points, splitArrangementLayoutSize(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:) takes absolute sizes, where the width values apply to horizontal splits and the height values to vertical ones. And splitArrangementFixedLayoutSize(horizontal:vertical:) asks the arrangement to prefer a view's own ideal size, which works well for a panel of controls that has a natural width.
Reading the Arrangement From Inside
The views you put in an arrangement often want to adapt to it. A panel that's a tall column in a horizontal split has very different proportions from the same panel in a short strip underneath. The splitArrangementAxis environment value tells a view which way it's being split, and it's nil when the view isn't in a split arrangement at all:
struct KitchenTimerPanel: View {
@Environment(\.splitArrangementAxis) private var splitAxis
var body: some View {
let layout = splitAxis == .horizontal
? AnyLayout(VStackLayout(spacing: 16))
: AnyLayout(HStackLayout(spacing: 16))
layout {
CountdownDial()
TimerControls()
}
}
}
When the split is horizontal, the panel is a narrow column, so the dial and controls stack vertically. Otherwise they sit in a row. Overlay arrangements have a matching environment value, overlayArrangementZIndex, which reports a view's position in the stack. Views with a higher z-index draw on top, so a floating card can use it to decide whether it needs its own background.
If the built-in styles don't fit, you can write your own by conforming to ArrangementViewStyle. Its makeBody(configuration:) hands you the primary and secondary content through the configuration.
When Not to Use One
Apple is specific about where arrangement views don't belong. Don't put one inside a NavigationSplitView, a List, a ScrollView, or any other container that could leave part of it unreachable. An arrangement view lays out content but doesn't do navigation, so navigation containers like tab views and split views should wrap it rather than live inside it.
That also answers a common question. If your two panes are a list and the detail for the selected row, you want NavigationSplitView, which already collapses on the outer display and adjusts its columns around the fold. ArrangementView is for two pieces of content that belong on screen together, not for drilling down from one to the other.
UIArrangementViewController in UIKit
UIKit gets the same container as UIArrangementViewController. You assign child view controllers to the primary and secondary placements, then apply an arrangement. Sizing works through view properties. Start from the arrangement's defaultViewProperties, adjust the width or height ranges using fractional or absolute dimensions, and set them back for a placement.
@MainActor
func makeRecipeArrangement() -> UIArrangementViewController {
let arrangement = UIArrangementViewController()
arrangement.setViewController(RecipeStepsViewController(), for: .primary)
arrangement.setViewController(IngredientsViewController(), for: .secondary)
var split = UISplitArrangement().axes(.horizontal)
var stepsProperties = split.defaultViewProperties
stepsProperties.width.preferred = .fractional(0.6)
stepsProperties.width.minimum = .absolute(320)
split.setViewProperties(stepsProperties, for: .primary)
arrangement.updateArrangement(split)
return arrangement
}
Switching to an overlay is a call to updateArrangement(.overlay, animated: true). Child view controllers can find their container through the new arrangementViewController property and call state(for:) on it to read the same information the SwiftUI environment values expose: the split axis, the z-index, and whether that placement is currently hidden.
Sample Project
Want to see this code in action? Check out the complete sample project on GitHub:
The repository includes a working Xcode project with all the examples from this article, plus unit tests you can run to verify the behavior.
Wrapping Up
ArrangementView is worth reaching for whenever a screen is two pieces of content that should share the display. Pick split for side-by-side content and overlay for layered content, restrict the axes when one orientation doesn't work, and let the environment values fine-tune the views inside. For content that doesn't fit this primary and secondary shape, the lower-level ReservedRegion API lets you position things around the fold yourself.
// Continue_Learning
Keeping Content Clear of the iPhone Duo Fold with ReservedRegion
iPhone Duo reserves parts of its displays for the fold and the front cameras. Learn which regions exist, when system views handle them for you, and how to query ReservedRegion in SwiftUI and UIKit to move custom content out of the way.
How to Get Your App Ready for iPhone Duo
iPhone Duo ships on October 23 with a folding inner display, a compact outer display, and toolbars that move to the side. Here's what to check in your app first, and where the new iOS 27.1 APIs fit in.
Building a Teleprompter for iPhone Duo's Outer Display with CameraCaptureAccessory
When iPhone Duo is open, its outer display faces the same way as the rear camera. CameraCaptureAccessory lets your app put content there while it records. Here's how to build a scrolling teleprompter with it in SwiftUI and UIKit.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.