# iPhone Duo API map

Every API this skill relies on, grouped by job, with the iOS version that
introduced it. *(27.1 beta)* marks symbols documented as beta in the iOS 27.1
SDK; their names and behaviour may change before release.

## Contents

1. Size, screen and shape
2. Reserved regions
3. Arrangements
4. Vertical bars
5. Sheets
6. Hinge
7. Scenes and accessories

## 1. Size, screen and shape

| Job | SwiftUI | UIKit | iOS |
|---|---|---|---|
| Size class | `horizontalSizeClass`, `verticalSizeClass` environment | trait collection size classes | 13 |
| Space actually available | `GeometryReader`, `onGeometryChange(for:of:action:)` | `view.bounds`, `viewDidLayoutSubviews`, `layoutSubviews` | 13 / 16 |
| Display scale | `displayScale` environment | `traitCollection.displayScale` | 13 / 8 |
| A window for a scene | | `UIWindow(windowScene:)` | 13 |
| The scene's screen, if truly needed | | `view.window?.windowScene?.screen` | 13 |
| Choose among layouts | `ViewThatFits`, `AnyLayout` | | 16 |
| Size relative to container | `containerRelativeFrame(_:alignment:)` | Auto Layout | 17 |
| Corners concentric with the device | `ConcentricRectangle` | `UICornerConfiguration`, `UIView.cornerConfiguration` | 26 |

`UIScreen.main` is deprecated as of iOS 26. Apple's guidance is to base layout
on the scene or containing view's bounds and on size classes, and not to use
`userInterfaceIdiom` or `UIInterfaceOrientation` for layout decisions.

## 2. Reserved regions

| Job | SwiftUI | UIKit |
|---|---|---|
| Region type | `ReservedRegion` *(27.1 beta)* | `UIView.ReservedRegion` *(27.1 beta)* |
| Query | `GeometryProxy.reservedRegions(kind:options:layoutDirectionBehavior:)` *(27.1 beta)* | `UIView.reservedRegions(kind:options:)` *(27.1 beta)* |
| Kinds | `.division` (fold), `.occlusion` (cameras) | same |
| Options | `.includeInactive` | same |
| Properties | `frame` (includes margins), `margins`, `isActive`, `kind`, `id` | `frame`, `margins`, `isActive`, `kind` (no `id`) |

```swift
struct FoldAwareGrid<Content: View>: View {
    @ViewBuilder var content: (_ columns: Int) -> Content

    var body: some View {
        GeometryReader { proxy in
            let hasFold = !proxy.reservedRegions(kind: .division, options: .includeInactive).isEmpty
            let base = proxy.size.width > proxy.size.height ? 3 : 2
            content(hasFold && base.isMultiple(of: 2) == false ? base + 1 : base)
        }
    }
}
```

Notes:

- The query returns regions that intersect the view whether active or not.
  Filter `isActive` when positioning for the current pose.
- `frame` already contains `margins`, the clearance kept for interactive
  content. Avoiding `frame` is enough for controls; inset `frame` by
  `margins` when full-bleed media may run up to the bare hardware area.
- SwiftUI mirrors region frames for right-to-left by default
  (`layoutDirectionBehavior: .mirrors`), so a custom `Layout` can compare them
  with subview frames directly. Pass `.fixed` for the physical frames.
- System containers (split views, lists, scroll views, sheets, alerts, menus)
  already avoid reserved regions; custom views and custom layouts are the
  ones that need this API.

## 3. Arrangements

| Job | SwiftUI | UIKit |
|---|---|---|
| Container | `ArrangementView(primary:secondary:)` *(27.1 beta)* | `UIArrangementViewController` *(27.1 beta)* |
| Choose style | `.arrangementViewStyle(.split)` / `.overlay` / `.automatic` | `updateArrangement(_:animated:)` with `UISplitArrangement` / `UIOverlayArrangement` |
| Limit axes | `.split.axes(.vertical)`, `.overlay.axes(...)` | `axes(_:)` on the arrangement |
| Assign content | the two view builders | `setViewController(_:for:animated:)` with `.primary` / `.secondary` |
| Split sizing | `splitArrangementLayoutRatio(_:)`, `splitArrangementLayoutSize(...)`, `splitArrangementFixedLayoutSize(...)` | `UISplitArrangement.Dimension` |
| Overlay details | `overlayArrangementEdge(_:)`, `overlayArrangementZIndex` environment | `state(for:)` |

```swift
ArrangementView {
    NowPlayingView()
} secondary: {
    LyricsView()
}
.arrangementViewStyle(.split)

ArrangementView {
    PlayerControls()
} secondary: {
    VideoSurface()
}
.arrangementViewStyle(.overlay)
```

The automatic style resolves to split. Split goes side by side when wider than
tall and stacks when taller than wide; overlay layers primary over secondary
when closed or fully open and places them on either side of the fold when
partially open (primary trailing or bottom, secondary leading or top).

Placement rules: navigation containers go around an arrangement, never inside
it; an arrangement does not go inside `NavigationSplitView`, `List`,
`ScrollView` or another container that could make part of it unreachable.

## 4. Vertical bars

| Job | SwiftUI | UIKit | iOS |
|---|---|---|---|
| Bars that can go vertical | `.toolbar` on `NavigationStack` / `NavigationSplitView` | items on a view controller in a navigation controller | |
| Custom Back or Close | `ToolbarItem(placement: .cancellationAction)` | `leadingItemGroups` | 14 / 16 |
| Prominent action | `topBarPinnedTrailing` | `pinnedTrailingGroup` | 27 / 16 |
| Group items | `ToolbarItemGroup` | `UIBarButtonItemGroup` | 14 |
| Title plus symbol | `Label` | `UIBarButtonItem` title and image | |
| Overflow order | `visibilityPriority(_:)`, `ToolbarItemVisibilityPriority` (`.automatic`, `.low`, `.high`, `init(higherThan:)`, `init(lowerThan:)`) | `visibilityPriority`, `UIBarButtonItemVisibilityPriority` | 27 |
| Put items in overflow | `ToolbarOverflowMenu` | `additionalOverflowItems` | 27 / 16 |
| Keep an item horizontal | `axisBehavior(_:)` with `ToolbarItemAxisBehavior` (`.automatic`, `.horizontalOnly`, `.verticalPreferred`) | `axisBehavior`, `UIBarButtonItem.AxisBehavior` | *(27.1 beta)* |
| Tab bar versus toolbar when tight | `toolbarVerticalCompressionBehavior(_:)` (`.automatic`, `.prefersTabBar`, `.prefersToolbarItems`) | `UIVerticalBarCompressionBehavior` | *(27.1 beta)* |
| Opt a screen out | `toolbarVerticalBehavior(.disabled)` | `preferredVerticalBarBehavior`, `setNeedsUpdateOfVerticalBarConfiguration()` | *(27.1 beta)* |
| Which edge the bar prefers | `toolbarVerticalEdge` environment (`HorizontalEdge?`) | `verticalBarEdge` trait (`UIVerticalBarEdge`) | *(27.1 beta)* |
| Image under the bar | `backgroundExtensionEffect()` | `UIBackgroundExtensionView` | 26 |

```swift
NavigationStack {
    EditorView()
        .toolbar {
            ToolbarItem(placement: .cancellationAction) {
                Button("Close", systemImage: "xmark") { dismiss() }
            }
            ToolbarItem(placement: .topBarPinnedTrailing) {
                Button("Done", systemImage: "checkmark") { save() }
            }
            ToolbarItemGroup {
                Button("Bold", systemImage: "bold") { toggleBold() }
                Button("Italic", systemImage: "italic") { toggleItalic() }
            }
            .visibilityPriority(.low)
        }
}
```

How items are shown: vertically with their icon; horizontally with icon or
title (icon preferred); in overflow with both. An item with only a title or a
custom view is never placed vertically.

Contexts that choose differently: inspectors keep horizontal bars; a split
view uses horizontal bars for sidebar and content columns and vertical bars
for the detail column; sheets on the outer display go vertical by default.

`toolbarVerticalBehavior` is resolved per container: the top view of a
`NavigationStack`, the selected tab of a `TabView`, the trailing column of a
`NavigationSplitView`. To hide bars rather than change their axis, use
`toolbarVisibility(_:for:)`.

`toolbarVerticalEdge` reports the preferred edge even when no vertical bar is
currently visible, and nil where the system never uses one:

```swift
@Environment(\.toolbarVerticalEdge) private var barEdge

FloatingPalette()
    .frame(maxWidth: .infinity, alignment: barEdge == .leading ? .leading : .trailing)
```

## 5. Sheets

On the inner display, sheets use horizontal toolbars when centred or leading
and vertical ones when trailing. Choose placement with
`presentationPlacement(_:)` (iOS 27) or `UISheetPresentationController.preferredPlacement`
(iOS 27). On the outer display, a sheet with a single action can opt out of the
vertical bar with `toolbarVerticalBehavior(.disabled)`.

## 6. Hinge

| | SwiftUI *(27.1 beta)* | UIKit *(27.1 beta)* |
|---|---|---|
| Observe | `onHingeChange(isEnabled:_:)`, old and new `DeviceHingeContext` | `UIHingeInteraction(updateHandler:)`, `(interaction, update)` |
| Hinge value | `DeviceHingeContext.hinge: DeviceHinge?` | `Update.hinge: UIHinge?` |
| Angle | `angle: Angle` | `angle: CGFloat`, radians |
| Status | `DeviceHinge.Status` struct: `.closed`, `.partiallyOpen`, `.fullyOpen` | `UIHinge.Status` enum: same plus `.unknown` |

```swift
@State private var hingeStatus: DeviceHinge.Status?

var body: some View {
    Canvas3DView(tilted: hingeStatus == .partiallyOpen)
        .onHingeChange { _, new in
            hingeStatus = new.hinge?.status
        }
}
```

- `hinge` is nil where there is no hinge; treat that as the normal phone case.
- The UIKit handler runs with the initial state, on changes, and when the
  interaction moves between hierarchies. It escapes, so capture weakly.
- Status comes from angle and orientation together; do not rebuild it from
  angle thresholds. Update rate and precision are system policy.
- Use the hinge for interactions and effects. Layout belongs to reserved
  regions, arrangements and size classes.

## 7. Scenes and accessories

- `sceneAccessory(content:)` (iOS 27) and UIKit's scene accessory
  registration (`registerSceneAccessory(_:)`) attach content to another
  display, for example a camera capture accessory on the outer display while
  the app runs on the inner one. See Apple's article on registering a camera
  capture accessory on iPhone Duo.
- New windows depend on what the current display allows. Prefer
  `UIWindowScene.ActivationAction` for "open in new window" menu items, and
  handle a failed scene request instead of assuming success.
