UIKit has first-party iPhone Duo support, and apps built on standard containers adapt with little work. The checklist below covers what still needs attention, and each item links to the detail.
UIKit iPhone Duo checklist
- Replace
UIScreen.main. UsetraitCollection.displayScaleand the window scene’s screen; the displays run at 430 and 460 ppi. - Audit
userInterfaceIdiomchecks. iPhone Duo reports the phone idiom with a regular-width display, so iPad layouts gated on idiom never appear. - Build with Xcode 27.1. Only the iOS 27.1 SDK reaches the screen edge and lays standard bar buttons out at the side.
- Inset by the safe area; never double one side. Left and right insets differ on iPhone Duo.
- Stay on standard bars. Custom
UIToolbarcontent is not placed in the side column. - Audit bar content on the closed outer display, using
axisBehavior, visibility priorities and the system overflow menu. - Adopt
UIArrangementViewControllerfor primary-and-secondary screens. - Keep content off the fold with
view.reservedRegions(kind: .division). - Take part in Split View. Remove
UIRequiresFullScreen, and do not stop visible content insceneWillResignActive. - Decide on multiple scenes. New windows open only on the inner display, and scene requests must handle errors.
- Adopt the camera coordinators in camera apps, so the right front camera stays active as the device folds.
- Run the App Resizability skill in Xcode 27.1, and review every change it makes.
- Treat App Store screenshots as optional for now. Apple lists iPhone Duo sizes, but uploads open later this year.
Where UIKit starts
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
Apple warns that safe areas are often asymmetric, especially on iPhone Duo: vertical buttons can appear on the left side in landscape and in Split View, so the two sides stop matching. Handle each side independently by insetting 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 closed outer display, and on the inner display in landscape, navigation, toolbar, and tab bar controls move into a single shared vertical region at the side. Only the inner display in portrait keeps horizontal bars. Picture your bars rotated ninety degrees into one column, and remember that the closed device shows a side column in both orientations.
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. Primary navigation goes first, and prominent actions directly after it. A navigation controller adds Back for you; for a custom Back or Close, use a leading item and keep leftItemsSupplementBackButton at its default of false:
navigationItem.leftItemsSupplementBackButton = false
navigationItem.leadingItemGroups = [UIBarButtonItemGroup(…)]
navigationItem.pinnedTrailingGroup = UIBarButtonItemGroup(…) // Done
Which axis each item uses. Items with a symbol go vertical, text-only items and custom views stay horizontal, and axisBehavior overrides that:
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.
Let a hero image run under the bar
Apple’s written guide, published on 18 September, adds something the Tech Talks did not say: if a view has a hero or background image, extend it under the vertical bar with UIBackgroundExtensionView rather than letting it stop at the bar’s edge. SwiftUI’s equivalent is backgroundExtensionEffect().
Where a sheet puts its bars
On the outer display the system gives a sheet vertical bars by default — opt out with preferredVerticalBarBehavior as above. On the inner display it depends on where the sheet sits: the toolbar stays horizontal for centered and leading placements, and goes vertical for trailing ones. Set preferredPlacement to tell the system where you want the sheet (presentationPlacement(_:) in SwiftUI).
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 UIWindowSceneActivationAction. 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. iPhone Duo still honours it, but not in the way the key implies: your app resizes anyway when the device opens and closes, and it scales on the inner display, including in Split View. The key buys a scaled app, not an exempt one.
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
grep -rn "UIScreen.main"and fix every hitgrep -rn "userInterfaceIdiom"and check each one against size classes instead- Rebuild against the iOS 27.1 SDK
- Run the App Resizability skill in Xcode 27.1 — it now covers this migration
- Audit bar content for the vertical region
- Adopt
UIArrangementViewControllerwhere 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.