THE DEVELOPER EDITION / REVIEWED 26 OCT 2026

Designing for iPhone Duo — condensed reference

This is an original summary of Apple's Human Interface Guidelines article Designing for iPhone Duo (first published September 9, 2026), the developer article Preparing your app for iPhone Duo, and Apple's iPhone Duo tech talks, written for AI coding agents. It preserves the guidance and the developer-doc links but not Apple's text or images. Read the sources for exact wording and diagrams:

Contents

  1. What the device is
  2. Best practices
  3. Dynamic layouts: reserved regions, folding, split and arrangement views
  4. Vertical controls
  5. Beyond the HIG: SDK, screens, bars, displacement, scenes, testing
  6. API reference: exact names and signatures
  7. Code patterns: UIKit bars, fold-aware layout, even-column grids
  8. Separate files: cameras and scene accessories, shipping to the App Store, system behaviors (several windows, Picture in Picture, Core Motion, widgets, web, accessibility)

1. What the device is

2. Best practices (the headline rules)

  1. Build to resize. Two displays, many poses, and Split View multitasking mean many sizes. Lay out with size classes, layout margins, and safe-area insets. No fixed widths, no display-specific dependencies.
  2. Consistent experience across displays. Same functionality and element state on both. Keep the information hierarchy; the larger inner display may show one extra level (Mail: list or message when closed, list and message when open).
  3. Same functionality in every pose. Controls may overflow and content may move or resize, but everything stays reachable.
  4. Accept the system's vertical bar layout. Standard components get it for free. Refine it, don't replace it.
  5. Games must be playable in every pose. Orientation lock is allowed, but fill the screen as the pose changes. Keep text and control sizes steady. Prefer changing the aspect ratio to letterboxing or pillarboxing; if you must box, fill the padding with artwork.

3. Dynamic layouts

Use layout margins and safe-area insets everywhere. Developer docs: SwiftUI GeometryProxy.safeAreaInsets, UIKit UIView.safeAreaInsets.

Reserved regions

Areas content must not cover, or that components adapt around (like iPad window controls). Three of them:

Region When present Behavior
Outer front camera Always Expands into the Dynamic Island for Live Activities. Side controls account for it automatically.
Inner front camera Only while the camera is active Invisible when inactive; the UI shifts aside when it activates.
Folding region Only while partially open Splits the inner display into usable areas on either side of the center.

Adapting when the device folds

Split views

Expand on the inner display, collapse to one pane on the outer display, exactly like regular ↔ compact on other iPhones. Built with standard components, they adapt to reserved regions automatically. Developer docs: SwiftUI NavigationSplitView, UIKit UISplitViewController.

Arrangement views

A layout container holding a primary and a secondary view, organized by display size, orientation, and reserved regions. Two kinds:

Guidance: - Use one when your layout already looks like one: HStack / VStack → split arrangement; ZStack → overlay arrangement. - Split fits main/detail; overlay fits foreground/background (a player over its queue). - Arrangement views don't navigate. Put a NavigationStack / TabView (or a UINavigationController) around them. The developer article says not to place one inside a NavigationSplitView, List, or ScrollView, where part of it could become unreachable. (The HIG names navigation split views among the containers to put around one; if you do, keep the arrangement fully reachable.) - Developer docs: SwiftUI ArrangementView, UIKit UIArrangementViewController. See §6.

4. Vertical controls

Toolbars, tab bars, and navigation controls move to the side on the outer display and on the inner display in landscape. Inner-display portrait keeps horizontal bars.

The side column holds, top to bottom: Dynamic Island, status bar, toolbar (including navigation buttons), tab bar.

In Split View multitasking on the inner display, each app puts its controls on its outer edge (left app → left edge, right app → right edge).

Side controls stay aligned with the hardware: same position relative to the outer camera, and the same physical side in right-to-left languages.

Rules:

5. Beyond the HIG: developer article and tech talks

Build and screens - Build with a Duo-capable Xcode (iOS 27.1 SDK or later). What each SDK gets on iPhone Duo (Apple's "Three steps" page):

Built with Open (inner display) Closed (outer display)
iOS 26 SDK or earlier centered, empty space around the window content left of the status bar and camera
iOS 27.0 SDK fills most of the display, still avoids the status bar on the right edge not stated
iOS 27.1 SDK or later full display, bars vertical below the status bar: what Apple calls optimized full display, side bars

Before shipping a 27.1 build, check that content adapts to the bars going vertical. - Re-read sizes whenever they change, not once at launch or in viewIsAppearing: a fold resizes the app mid-session. Lay out in layoutSubviews / viewDidLayoutSubviews (SwiftUI: GeometryReader, onGeometryChange); use viewWillTransition(to:with:) for work that must run on a size change. Create windows with UIWindow(windowScene:), not UIWindow(frame: UIScreen.main.bounds), and don't store a UIScreen. - UIScreen.main is deprecated since iOS 26.0 and ambiguous on a two-display device (talk 111461 still says "will be deprecated"; the API docs list it as deprecated). Use the view or scene bounds, window?.windowScene?.screen, and traitCollection.displayScale. - UIRequiresFullScreen is still honored, but the app resizes anyway when the device opens or closes. - Orientation locks: talk 111461 says both that the inner display doesn't honor supported orientations and that locked apps are scaled there. Either way, don't rely on the lock. Don't branch layout on userInterfaceIdiom or interface orientation. Use size classes with automatic trait tracking. - Orientation doesn't give the app's shape. Remove layout logic based on UIDevice.current.orientation, statusBarOrientation, or interfaceOrientation (including windowScene.effectiveGeometry.interfaceOrientation); to know whether the app is wider than tall, compare the width and height of the space you have. - Size classes don't identify the device: with iPhone Duo and resizable iPhone Mirroring, an iPhone app can get any combination. Code that shows an iPad-only layout at regular width, or assumes iPhone is always compact, now runs that layout on iPhone too, so make sure it works there (Apple: the inner display should look like a natural extension of the iPad layout). Drop comparisons with known iPhone sizes (bounds.height == 844); none match. - Full-bleed media (.scaleAspectFill, .aspectRatio(contentMode: .fill)) can lose important content on the wider displays. Choose fill or fit from the size classes or aspect ratio, or set a focal point. - Safe-area insets differ per side. Use bounds.inset(by: safeAreaInsets), not width - insets.left * 2. - Corners: ConcentricRectangle (SwiftUI) and UICornerConfiguration (UIKit) follow the new screen shapes.

Bars - Only bars owned by a container go vertical: SwiftUI .toolbar on content inside NavigationStack, NavigationSplitView, or TabView; UIKit items on a view controller inside a UINavigationController / UITabBarController. Content of a custom UIToolbar, UINavigationBar, or UITabBar is ignored. - Split views: only the detail column's bars go vertical; sidebar and content columns stay horizontal. Inspectors stay horizontal. Sheets on the outer display go vertical by default. On the inner display, only trailing sheets do (presentationPlacement(_:) / UISheetPresentationController.preferredPlacement). - Top of the side bar: custom Back/Close as ToolbarItem(placement: .cancellationAction) or UIKit leadingItemGroups with leftItemsSupplementBackButton = false; prominent actions (Done) as .topBarPinnedTrailing or UINavigationItem.pinnedTrailingGroup. - The side bar uses the icon; horizontal bars prefer the icon; overflow uses icon and title. Title-only items and custom views stay horizontal unless opted in with axisBehavior(.verticalPreferred). Use .horizontalOnly for items whose text carries meaning (a cart total, a Select/Done toggle). - Flexible spacers are zero-size vertically; fixed spacers keep their size. Don't add spacing. - Show counts with .badge(_:) / UIBarButtonItem.badge = .count(n). - Under Reduce Transparency the side bar gets a background; keep custom content legible. - Disable side bars (.toolbarVerticalBehavior(.disabled) / override preferredVerticalBarBehavior to return .disabled) only for immersive single-purpose UIs such as a calculator, a full-screen player, or a sheet with a single button. Treat it as a fixed choice, not something that toggles with view state. - Custom views that must adapt to the side bar read @Environment(\.toolbarVerticalEdge) (HorizontalEdge?) or traitCollection.verticalBarEdge (.leading, .trailing, .unspecified). It's the direct signal for "are the bars vertical right now", better than size class or orientation (Apple DTS): the outer display stays compact width in both orientations. - Hero or background images extend under the side bar with backgroundExtensionEffect() / UIBackgroundExtensionView. - Information-dense apps can show the tab bar as a sidebar on the inner display: TabView { … }.tabViewStyle(.sidebarAdaptable).defaultTabBarPlacement(.sidebar) (27.0; takes an AdaptableTabBarPlacement) / tabBarController.sidebar.preferredPlacement = .sidebar.

Displacement and poses - When the fold would cover something, move the smallest meaningful scope and move related elements together. - Continuous scrolling content (feeds, articles, lists) doesn't need to avoid the fold. - Grids: query inactive divisions (options: .includeInactive) to prefer an even column count whenever a fold exists (talk 111463). Apple DTS doesn't recommend making scrolling grids avoid the fold; there's no automatic fold avoidance in UICollectionView. Adjust only sections that don't scroll across the fold, from the collection view's reserved regions. - Poses: the inner display is wider than tall when opened like a book, so book pose (vertical fold) is landscape with side bars, and tabletop / laptop pose (horizontal fold) is portrait with horizontal bars. - Book pose: displaced alerts go to the trailing side. Tabletop pose: the top half suits content viewed from a distance, the bottom half suits controls. A tabletop layout is optional and keeps the same controls. - Audit centered layouts: they're what the fold cuts through. - Hinge state (onHingeChange / UIHingeInteraction) is for interactions, such as using the hinge angle as an input. Don't drive layout from it, and don't depend on its update rate or precision: both are system policy (Apple DTS).

Scenes and state - Built with the SDK after iOS 26, an app that hasn't adopted the UIScene life cycle won't launch (TN3187). SwiftUI Apps already use scenes; UIKit apps need a UIApplicationSceneManifest or configurationForConnecting. - Opening or closing doesn't change scenePhase for a full-screen app: the scene just resizes through size-class and trait changes. With two windows on the inner display, closing keeps the most recent one active and moves the other to inactive, then background (Apple DTS). Keep UI state in the model so it survives resizes. - ArrangementView shows only one view when its split axes can't fit the aspect ratio, pose, or compact size class: the one with the higher .layoutPriority, else the primary (Apple DTS). Observe \.splitArrangementAxis and keep the hidden view's actions reachable elsewhere. - toolbarVerticalBehavior is resolved per window or presentation (a NavigationStack uses its top view, a TabView its selected tab), so disabling it for one immersive screen belongs on a full-screen cover or sheet.

Testing - Xcode 27.1 is the released baseline. The 27.2 RC notes also document Duo simulator support; the earlier beta exclusion is obsolete. Checked 2026-10-26. Poses come from Device Hub; Xcode Previews have a "Display" override for the outer display. - In 27.2 RC, captures can stay black for several minutes after boot (187146039). Check the active display and retry after startup. Modifier keys can stop after a display switch; click the visible screen to restore them (189465196). - Device Hub cannot expose Duo screen content to VoiceOver or Accessibility Inspector in 27.2 RC (187148389). Verify accessibility directly on hardware; a Device Hub check alone is insufficient. - Mac Catalyst compilation can fail for iOS 27.1-specific APIs in 27.2 RC (185924957). Isolate affected code with #if !targetEnvironment(macCatalyst) and keep a Catalyst fallback; runtime availability checks alone won't fix it. - Xcode 27.2 defaults to JSON .xcproj project configuration (format documentation). The audit reads .pbxproj and .xcproj full-screen settings; JSON projects also contribute scene-generation evidence. Python 3 is required only for .xcproj. It handles Xcode-emitted comments and trailing commas; unsupported JSON5 syntax or malformed input exits 2. Review inherited/conditional settings and .xcconfig values in the actual build; this is a candidate scan, not a build-settings resolver. No migration is needed for Duo. - Without the Duo simulator, Apple lists two ways to test resizing: the resizable iOS simulator in Device Hub, and iPhone Mirroring on macOS 27 (drag the window to extreme sizes both ways). They exercise resizing; don't assume they show side bars, the fold, or the cameras. Apple calls the iPhone Duo simulator the best way to test the full native experience. - Xcode 27.1's coding assistant ships Apple's own app resizability skill ("get my app ready for iPhone Duo"; xcrun agent skills export writes it out for other agents). It scans for the resize patterns above (main screen, orientation, idiom and device-size checks, full-bleed media) and, by Apple's account, doesn't find everything. Use it alongside this skill, which adds the bars, fold, arrangement-view, and camera guidance. - Simulator known issues (Xcode 27.1 notes): StandBy unavailable, most app extensions (widgets, Live Activities) can't run or be debugged. Verify those on a device; these restrictions are recorded for 27.1, not asserted for every newer runtime. - Screenshots: xcrun simctl io <device> enumerate lists displays; pass screenshot --display=<id> to capture the one in use.

Cameras and scene accessories: cameras-and-scene-accessories.md (tech talks 111464–111465). App Store submission: shipping.md. Several windows, Picture in Picture, Core Motion, widgets, web content, accessibility: system-behaviors.md. Don't hard-code "Face ID" in UI text; read LAContext().biometryType.

6. API reference

Checked against Apple's developer docs on 2026-10-21. iOS 27.1 (final release in Xcode 27.1) unless noted. Guard with if #available(iOS 27.1, *) when the deployment target is lower.

Reserved regions - SwiftUI: GeometryProxy.reservedRegions(kind: ReservedRegion.Kind, options: ReservedRegion.QueryOptions = [], layoutDirectionBehavior: LayoutDirectionBehavior = .mirrors) -> [ReservedRegion], called inside a GeometryReader. - UIKit: UIView.reservedRegions(kind: UIView.ReservedRegion.Kind, options: UIView.ReservedRegion.QueryOptions = []) -> [UIView.ReservedRegion]. - Region properties: frame (includes margins), margins, isActive, kind, id. Kinds: .division, .occlusion. Options: .includeInactive (active regions only by default, per QueryOptions and talk 111463; the ReservedRegion overview's "regardless of whether they are currently active" contradicts both). The fold is active only while partially folded; when flat it's inactive with zero width.

Arrangement views - SwiftUI: ArrangementView<Primary, Secondary>, written ArrangementView { Primary() } secondary: { Secondary() }. Style with .arrangementViewStyle(_:): .automatic (resolves to split), .split, .overlay; limit axes with .split.axes(.horizontal) / .overlay.axes(_:). Custom styles conform to ArrangementViewStyle. - Split sizing: .splitArrangementLayoutRatio(_:), .splitArrangementLayoutSize(minWidth:…), .splitArrangementFixedLayoutSize(horizontal:vertical:). Overlay: .overlayArrangementEdge(_:) picks the horizontal edge a view takes when side by side. - Environment: \.overlayArrangementZIndex (Int; greater than 0 means this view is drawn on top, so it can collapse itself, as talk 111463 does with an Up Next queue placed in the primary slot), \.splitArrangementAxis. - UIKit: UIArrangementViewController(); setViewController(_:for: .primary / .secondary, animated:); updateArrangement(_:animated:) with UISplitArrangement (default) or UIOverlayArrangement, e.g. .split.axes(.horizontal); state(for:) returns a ViewState? with isHidden, splitAxis, zIndex.

Hinge - SwiftUI: .onHingeChange(isEnabled:_:) with (DeviceHingeContext, DeviceHingeContext); context.hinge is a DeviceHinge? (nil without a hinge) with angle and status (.closed, .partiallyOpen, .fullyOpen). - UIKit: UIHingeInteraction(updateHandler:) added with addInteraction(_:); update.hinge is a UIHinge? with angle (radians) and status (adds .unknown).

Bars | Purpose | SwiftUI | UIKit | |---|---|---| | Overflow order | ToolbarContent.visibilityPriority(_:) (27.0) | UIBarButtonItem.visibilityPriority (27.0) | | Explicit overflow items | ToolbarOverflowMenu { … } (27.0) | UINavigationItem.additionalOverflowItems (16.0) | | Vertical eligibility | ToolbarContent.axisBehavior(_:): .automatic, .horizontalOnly, .verticalPreferred | UIBarButtonItem.axisBehavior (same cases) | | Tab bar vs items | .toolbarVerticalCompressionBehavior(_:): .automatic, .prefersTabBar, .prefersToolbarItems | UINavigationItem.verticalBarCompressionBehavior: .automatic, .prefersTabBar, .prefersBarItems | | Opt out of side bars | .toolbarVerticalBehavior(_:): .automatic, .disabled | UIViewController.preferredVerticalBarBehavior (override) | | Which edge | EnvironmentValues.toolbarVerticalEdge | UITraitCollection.verticalBarEdge | | Pinned prominent action | ToolbarItemPlacement.topBarPinnedTrailing (27.0) | UINavigationItem.pinnedTrailingGroup (16.0) | | Sheet placement | .presentationPlacement(_:) (27.0) | UISheetPresentationController.preferredPlacement (27.0) |

No group-level visibility priority is documented on UIBarButtonItemGroup; set it on each item. additionalOverflowItems is a UIDeferredMenuElement?; setting it shows the overflow button.

Also confirmed by the HIG: NavigationSplitView, UISplitViewController, ToolbarItemGroup, UIBarButtonItemGroup, Label, UIBarButtonItem, GeometryProxy.safeAreaInsets, UIView.safeAreaInsets. Anything else Duo-specific: look it up in the SDK and never guess.

7. Code patterns

Uncompiled sketches built only from the APIs in §6. Adapt names to the project.

UIKit bars. Title + image, groups instead of spacers, extras in the system overflow. Bottom-bar actions stay toolbar items (toolbarItems); top-bar actions stay navigation items.

// UIKit: title + image, groups instead of spacers, extras in the system overflow.
let compose = UIBarButtonItem(title: "Compose", image: UIImage(systemName: "square.and.pencil"),
                              primaryAction: UIAction { [weak self] _ in self?.compose() }, menu: nil)
compose.visibilityPriority = .high
navigationItem.trailingItemGroups = [UIBarButtonItemGroup(barButtonItems: [compose], representativeItem: nil)]
navigationItem.additionalOverflowItems = UIDeferredMenuElement.uncached { done in
    done([UIAction(title: "Print", image: UIImage(systemName: "printer")) { _ in }])
}
let arrangement = UIArrangementViewController()   // root of a UINavigationController; .secondary works the same
arrangement.setViewController(PlayerViewController(), for: .primary)

Fold-aware custom layout (UIKit). Regions are in this view's coordinates. Re-read them on every layout pass; no change callback is documented.

override func layoutSubviews() {
    super.layoutSubviews()
    let safe = bounds.inset(by: safeAreaInsets)
    var target = safe                                          // where the palette group may sit
    if #available(iOS 27.1, *) {
        // Active fold only: nothing moves while the device is flat.
        // Filter isActive too: one doc overview says inactive regions can be returned by default.
        if let fold = reservedRegions(kind: .division).first(where: \.isActive)?.frame {
            if fold.height >= fold.width {                     // vertical fold (book pose): use the trailing half
                target = CGRect(x: fold.maxX, y: safe.minY, width: safe.maxX - fold.maxX, height: safe.height)
            } else {                                           // horizontal fold (tabletop): controls go below
                target = CGRect(x: safe.minX, y: fold.maxY, width: safe.width, height: safe.maxY - fold.maxY)
            }
        }
    }
    palette.center = CGPoint(x: target.midX, y: target.midY)  // move the whole group together
    // Also keep `palette.frame` clear of reservedRegions(kind: .occlusion) (cameras) the same way.
}

Even-column grid (UIKit compositional layout).

func columnCount(for width: CGFloat, minItemWidth: CGFloat = 100, spacing: CGFloat = 2) -> Int {
    var count = max(1, Int((width + spacing) / (minItemWidth + spacing)))
    if #available(iOS 27.1, *),
       !collectionView.reservedRegions(kind: .division, options: .includeInactive).isEmpty,
       count > 1, count % 2 == 1 {
        count += 1                                            // the fold falls between two columns
    }
    return count
}
// Width: collectionView.bounds.inset(by: collectionView.safeAreaInsets).width, per side, never left * 2.
// Rebuild the layout when the width or size class changes (viewDidLayoutSubviews / registerForTraitChanges).
// The grid scrolls, so it doesn't need to avoid the fold: even columns are enough (Apple DTS).

SwiftUI. Read regions inside a GeometryReader (proxy.reservedRegions(kind: .division)) or pass them to a custom Layout; they're mirrored for right-to-left by default (layoutDirectionBehavior: .fixed to opt out).