SwiftUIUIKitiPhone Duo

The vertical bar on iPhone Duo

How iPhone Duo moves navigation, toolbars, and tabs to the side, which containers qualify, and how to place sheets and floating buttons.

Oct 4, 20269 min read10 sources

On iPhone Duo the chrome moves. Navigation items, toolbar actions, and tabs can leave the top and bottom of the display and share one column on the side, which gives the content the height back. This happens when you build with the iOS 27.1 SDK. You keep declaring items in the same placements. SwiftUI and UIKit decide where they go.

This page is that decision, plus the cases where developers found the system will not help. For the overall layout, start with designing for iPhone Duo. For the crease itself, see the fold guide.

In short

  • Rebuild with the iOS 27.1 SDK and keep bars in NavigationStack, NavigationSplitView, TabView or their UIKit controllers. Those move. A bar you built does not.
  • The side column appears on the outer display and on the open phone held sideways. Open and upright, the bars go back to the top and bottom.
  • Symbol buttons turn on their own. Text and custom views stay horizontal unless you say .axisBehavior(.verticalPreferred).
  • Turning the column off is a per-presentation choice. Make it once, for the whole screen.
  • Sheets adapt by themselves and can be placed leading, trailing or centre. Floating buttons are the one piece you still place by hand.

Only bars the system owns will move

A toolbar on a NavigationStack, NavigationSplitView, or TabView is in. A UINavigationController or UITabBarController is in. A UIToolbar, UINavigationBar, or UITabBar you created and stuck in the hierarchy is out. Its items are never considered for the side column. Apple’s engineers said the same thing in the September group lab: a custom tab bar does not hop over by itself. You would be reimplementing the metrics.

The column is shared. Navigation, toolbar actions, and tabs occupy one strip, not three. It shows up where horizontal space is cheap and vertical space is not: the outer display, and the inner display in landscape. On the inner display in portrait, bars go back to the familiar top and bottom. People see that change every time they turn the open phone. Build for it instead of fighting it.

The Detail app on the open iPhone Duo: a video on the left, its transcript on the right, and the app’s buttons in a column down the right edge
Detail on the open phone, held sideways. Back, the app’s actions and the status icons share one column down the right edge.Image: Apple

Sarun W., working from the Raise the Bar session, adds a constraint that is easy to miss in a split view. A bar moves when its container sits on the display edge. The detail column gets the vertical bar. The other columns keep a horizontal bar, and so does an expanded inspector, so it does not compete with the detail column. If a button “refuses” to move, check which column it lives in before you reach for an opt-out.

Slack on the open iPhone Duo: the sidebar of direct messages on the left, a conversation on the right
Slack on the open phone. The sidebar keeps its buttons in a row along the top, and the conversation beside it has its buttons down the side.Image: Apple

The column belongs to the hardware, not the reading direction. In a right-to-left language it stays on the same side of the device, and the content adapts around it. Inside it, keep Apple’s order from top to bottom: navigation such as Back or Close first, then a prominent action such as Done, then everything else in its original groups.

A horizontal bar has a fixed height and a flexible item width. A vertical bar swaps that. It has a fixed width and a flexible item height, so it wants symbols, not sentences.

Tell the system which items can turn

Standard symbol buttons can join the side column on their own. Text-only items, and custom views the system cannot resize, stay horizontal. A segmented control or a text button that is too wide stays in the horizontal bar even when everything around it has moved. If you already give a button both a title and an image, through a Label or the matching UIBarButtonItem properties, you are most of the way there. The system picks the form that fits.

When you want a different answer, set it.

.toolbar {
    ToolbarItem(placement: .bottomBar) {
        Compass()
    }
    .axisBehavior(.verticalPreferred)

    ToolbarItem {
        SelectOrDoneButton()
    }
    .axisBehavior(.horizontalOnly)
}

.verticalPreferred is how you opt a custom view in. .horizontalOnly keeps an item horizontal even when it has an icon, which is what you want for a control that swaps between a symbol and the word Done. Khoa Pham notes a third trick from the same session: a custom view that mixes an icon with a count can use badge() so the count becomes a badge and the icon can join the column.

Inside the custom view, read where you ended up.

struct Compass: View {
    @Environment(\.toolbarVerticalEdge) private var edge

    var body: some View {
        let layout = edge == nil ? AnyLayout(HStackLayout()) : AnyLayout(VStackLayout())
        layout {
            Image(systemName: "location.north")
            Text("N")
        }
    }
}

toolbarVerticalEdge is leading, trailing, or nil. Nil means the bars are horizontal right now, so draw the horizontal version. UIKit exposes the same fact as traitCollection.verticalBarEdge. If you set .horizontalOnly, keep the horizontal drawing even when the environment has an edge. The item is not in the column.

A few placements are worth knowing by name. cancellationAction stays at the top of the column. In UIKit that is a leading item group with leftItemsSupplementBackButton set to false, so your close button takes the back button’s place instead of sitting next to it. topBarPinnedTrailing, and UIKit’s pinned trailing group, keeps a primary action such as Share from falling into the overflow. visibilityPriority decides who survives as the column runs out of room. By default, items overflow from the bottom upward.

When the column runs out of room

Tabs and toolbar items share the strip, and there is often not enough of it. SwiftUI can push toolbar items into an overflow menu, or collapse the tabs into a single switcher. toolbarVerticalCompressionBehavior chooses which happens first.

On iOS the default favors the tab bar: toolbar items overflow, tabs stay visible. Ask for that explicitly with .prefersTabBar, or flip it with .prefersToolbarItems when the actions are the product and the tabs can collapse. Natalia Panferova’s toolbar post shows both on the same editor. To choose which individual items survive the squeeze, set visibilityPriority on them.

If you have your own overflow actions, fold them into the system menu with ToolbarOverflowMenu instead of drawing a second menu. UIKit’s version is additionalOverflowItems.

Turning the column off

.toolbarVerticalBehavior(.disabled) sends the bars back to the top and bottom and restores a horizontal status bar. UIKit’s equivalent is preferredVerticalBarBehavior returning .disabled.

Use it when the side column costs you width you actually need: a video player, a camera, a single close button on a sheet, a layout like Weng Tianxin’s photo frame in the design guide. Apple’s recommendation, repeated by Natalia, is to keep the vertical bars for most interfaces. People will learn the column from everyone else’s apps. A fixed bar makes yours feel like it missed the update.

If you do opt out, do it for the whole presentation and leave it. The preference resolves at the window, or at the nearest presentation such as a sheet. Within a window, SwiftUI takes it from the top view of a NavigationStack, the selected tab of a TabView, or the trailing column of a NavigationSplitView, so changing it as someone pushes a new screen makes the chrome flip in the middle of a task. Artem Novichkov saw the same thing in the simulator: the setting flows up to the window or the nearest presentation, which is why Artem’s sample presents that screen full screen.

Applied inside a sheet, the modifier affects the sheet only. The screen underneath can keep the default.

Sheets

Sheets pick up the same adaptations with no extra code, and then they add a placement.

On the outer display, a sheet’s controls go to the side. On the inner display, a centered sheet keeps a horizontal toolbar. As the phone folds partway, the system slides the sheet off the crease. You do not have to watch the hinge to get that.

presentationPlacement, from iOS 27, takes automatic, center, leading, or trailing. A gallery can open a sheet on the same side as the thumbnail the person tapped. Placement also changes the toolbar. Apple’s documentation on preparing your app says that on the inner display, centered and leading sheets use a horizontal toolbar, and a trailing sheet uses a vertical one.

In the iOS 27.1 beta, Natalia saw trailing sheets miss that vertical toolbar in both the flat and the partly folded poses. The close button could sit under the status display and the camera. One line cleared it in Natalia’s testing:

.sheet(item: $selection) { item in
    DetailSheet(item: item)
        .presentationPlacement(.trailing)
        .presentationDetents([.large])
}

Natalia is explicit that the detent is a workaround for this beta, not a requirement of placement. Check a trailing sheet in your own build before you copy it in, and again when the SDK updates. The checklist includes that pass.

Rob Swift hit the other sheet failure: a custom height that does not sit in the safe area. If you are setting a height by hand, look at it open, closed, and partly folded before you ship the number.

Floating buttons are yours to place

This is the gap everyone adapting a real app runs into. Harshil’s open layout behaved like an iPad. The closed layout was mostly a matter of the new horizontal insets. The control left over was a floating share button, and the open question was how much padding it needs to line up with the toolbars.

Rob Swift’s screens show the same family of bugs. Two trailing bar items laid out oddly. A bell drawn as a custom view pushed the button that was supposed to be at the top down beneath it. A floating action button sat high, because the bottom safe area is tall even when the toolbar has left the bottom edge. Matthaus Woolard, replying to Harshil, would put that share button in safeAreaBar(edge: .bottom) so the scroll view insets itself, the bar blurs locally, and the padding comes from the system. Matthaus also expects Apple would rather you move Share into the side column when there is room.

Rob Swift@RobSwish

I use size classes and I'm still finding the Duo to be completely different animal than iPhone or iPad. Here you see 2 right bar button items laying out very oddly on the Duo.

Image from Rob Swift's post

If the bottom inset is still reserving space for a toolbar that is no longer at the bottom, treat it as a bug and file it, which is what Matthaus suggested on that thread. Then decide whether the button should be a toolbar item. A plus button that adds to a list belongs with that list, as a toolbar action, more often than it belongs in a circle positioned in a ZStack.

Sahil AK’s TimeWave adaptation is the short version. The native toolbar APIs did the work, and the floating plus was what remained. The post is in the design guide.

Quick reference

You want to SwiftUI UIKit
Let a custom item join the column .axisBehavior(.verticalPreferred) on the item
Keep an item horizontal even with an icon .axisBehavior(.horizontalOnly)
Draw a custom view for the column read @Environment(\.toolbarVerticalEdge) traitCollection.verticalBarEdge
Put a close button at the top of the column .cancellationAction placement a leading group, leftItemsSupplementBackButton = false
Keep a primary action out of the overflow .topBarPinnedTrailing navigationItem.pinnedTrailingGroup
Turn an icon-plus-count view into icon and badge .badge() UIBarButtonItem.badge
Choose what collapses first, tabs or actions .toolbarVerticalCompressionBehavior(.prefersTabBar) or .prefersToolbarItems
Choose which items survive the squeeze .visibilityPriority()
Fold your own actions into the system overflow ToolbarOverflowMenu additionalOverflowItems
Send the bars back to the top and bottom .toolbarVerticalBehavior(.disabled) preferredVerticalBarBehavior returning .disabled
Place a sheet .presentationPlacement(.leading / .center / .trailing)

Blank UIKit cells are features the sources above only describe in SwiftUI. Check Apple’s UIKit documentation before you assume there is no equivalent.

Keep reading

Ideas · Design5 min read

iPhone Duo app ideas: 6 things builders are doing with the fold

Over 300 demos, concepts and prototypes for iPhone Duo, sorted into the six patterns that keep coming up, from the half-folded laptop to the hinge as a controller. A starting point if you're deciding what to build.

Oct 6Read the guide

Apps · Specs4 min read

Does iPhone Duo run iPad apps? The Duo and the iPad mini

iPhone Duo's inner display is nearly iPad mini sized, but it runs iOS, not iPadOS. What that means for iPad-only apps, why many Duo apps will look like their iPad versions, and what the Duo borrows from iPad.

Oct 6Read the guide