Skip to content
iPhone Duo SupportGet help

UIKit on iPhone Duo: vertical bars and arrangements

Ingenious Techlab Team

UIKit apps have an advantage on iPhone Duo that is easy to miss: if your app already supports iPad, most of the work is done. The inner display is regular-width in both dimensions, which is the same environment your iPad layout already targets.

The apps with real work ahead are iPhone-only ones, and apps that replaced the system bars with their own.

UIScreen.main stops making sense

Start here, because it is the change with the widest blast radius.

Apple stated in Prepare your app for iPhone Duo that main screen references will be deprecated and should be avoided on two-display devices. This is not a style preference. iPhone Duo has two displays with different pixel densities — 430 ppi inner, 460 ppi outer — and your app can move between them while running. A global “main screen” cannot answer which display a given view is actually on.

// Before
let scale  = UIScreen.main.scale
let bounds = UIScreen.main.bounds

// After
let scale  = traitCollection.displayScale
let screen = view.window?.windowScene?.screen

Scale is a trait, so it updates automatically as the view moves between displays. That is the whole point of reading it from the trait collection rather than a global.

For layout, do not reach for the screen at all. view.bounds and the safe area describe the space you were given — which in Split View is smaller than the display regardless of which one you are on.

grep -rn "UIScreen.main" --include="*.swift" .

Run that first. It is usually a bigger list than people expect, and much of it is in code nobody has opened in years.

Safe areas are asymmetric

On every previous iPhone, left and right insets were mirror images. That made a shortcut look safe:

// Wrong on iPhone Duo
let width = view.bounds.width - view.safeAreaInsets.left * 2

The status bar and camera sit asymmetrically on the inner display, and the hinge introduces geometry with no mirror. Inset the rectangle instead of doing arithmetic on one side:

let width = view.bounds.inset(by: view.safeAreaInsets).width

Apple’s split is worth stating plainly, because it is what makes an app look finished:

// Interactive content — inside the safe area
foreground.frame = view.bounds.inset(by: view.safeAreaInsets)

// Background artwork — full bounds
backgroundView.frame = view.bounds

Applying one policy to a whole screen is the mistake. A card with a background image wants the image to bleed and the text to stay inset.

The vertical bar region

This is the most visible change under the iOS 27.1 SDK. On the inner display in landscape, navigation, toolbar, and tab bar controls move into a single shared vertical region at the side. Picture your bars rotated ninety degrees into one column.

You get this automatically with UINavigationController and UITabBarController. Custom UIToolbar content is not considered. If you replaced the system bars with your own — a common decision in apps with strong visual identity — you are outside the adaptive path and will need to rebuild on the system containers to get back into it.

Two details worth knowing: the bar stays on the same side in right-to-left languages, and in a split view only the detail column participates. Inspectors do not get their own bar.

Controlling it

Placement, since order matters in a column:

// Back / close goes to the top
navigationItem.leftBarButtonItem = closeItem   // .cancellationAction equivalent

Orientation of individual items:

item.axisBehavior = .verticalPreferred
item.axisBehavior = .horizontalOnly

Detecting the vertical case, so custom views can adapt:

switch traitCollection.verticalBarEdge {  }

What compresses first when space runs out:

navigationItem.verticalBarCompressionBehavior = .prefersBarItems

Which actions survive overflow:

item.visibilityPriority = .high

And consolidating your own ellipsis menu into the system one, so users do not see two identical menus stacked:

navigationItem.additionalOverflowItems = UIDeferredMenuElement({ provider in
    provider(self.persistentOverflowItems())
})

Badges from iOS 26 carry over, and are useful for keeping a signal on an item that has lost its text label:

item.badge = .count(7)

Opting out

Some screens are worse for it. Apple names single-page layouts with heavy bottom content — a calculator — and sheets with only a close button:

override var preferredVerticalBarBehavior: UIVerticalBarBehavior { .disabled }

Per screen, on purpose. Not globally.

Arrangements

UIArrangementViewController is the UIKit counterpart to SwiftUI’s ArrangementView, and it handles the main-and-secondary case across every pose:

let arrangementVC = UIArrangementViewController()
let navController = UINavigationController(rootViewController: arrangementVC)

arrangementVC.setViewController(PlayerViewController(), for: .primary)
arrangementVC.setViewController(UpNextViewController(), for: .secondary)

Style is set the same way, and can be updated at runtime:

arrangementVC.updateArrangement(.split.axes(.horizontal))

For an overlay arrangement, read the z-index so the secondary view can collapse when stacked:

let primaryState = arrangementVC.state(for: .primary)
myModel.minimization = (primaryState?.zIndex ?? 0) > 0 ? .collapsed : .expanded

Reserved regions

UIViewReservedRegion reports where the display is interrupted:

let regions = view.reservedRegions(kind: .division)
let frames  = regions.map(\.frame)

Division regions divide — the hinge. Occlusion regions cover — the under-display camera. Division regions are active only while folded and have zero width when flat, so handle them appearing and disappearing, not just moving.

Use arrangements first. Query regions directly when you need control an arrangement cannot give you.

Multiple scenes

iPhone Duo is the first iPhone supporting multiple app instances. Apps already supporting this on iPad get it here.

<key>UIApplicationSupportsMultipleScenes</key>
<true/>

Two constraints: new windows can only be created on the inner display, and Apple is explicit that you must handle errors when requesting a scene through UIWindowSceneActivation. The action hides itself when windows are unavailable, so a correctly built UI does not offer something that cannot work.

If you enable it, audit anything assuming a single instance — singletons holding view state, global notification handling that presents UI, analytics assuming one session per launch.

Split View is not optional

All apps participate in side-by-side multitasking on iPhone Duo. An app that has never been resized can now be handed an arbitrary fraction of the display, at a moment the user chooses.

override func viewWillTransition(
    to size: CGSize,
    with coordinator: UIViewControllerTransitionCoordinator
) {
    super.viewWillTransition(to: size, with: coordinator)
    coordinator.animate { _ in self.relayout(for: size) }
}

While you are there, check UIRequiresFullScreen. An app still carrying that key is fighting the platform.

One failure mode that is easy to miss: in Split View your app can be visible but not frontmost. Timers, video, and animation stopped on sceneWillResignActive will look frozen to a user who can still see the window.

Where to start

  1. grep -rn "UIScreen.main" and fix every hit
  2. grep -rn "userInterfaceIdiom" and check each one against size classes instead
  3. Rebuild against the iOS 27.1 SDK
  4. Run the App Resizability skill in Xcode 27.1 — it now covers this migration
  5. Audit bar content for the vertical region
  6. Adopt UIArrangementViewController where you have a primary/secondary screen

Steps 1 and 2 are unglamorous and account for most of the difference between an app that behaves and one that does not.

Related

Want this done for you?

We add iPhone Duo and adaptive-layout support to existing iOS apps, native and cross-platform.