iphone-duo-dual-pane-patterns
Design and build two-pane experiences for iPhone Duo — the real pixel and point dimensions of each display and each half, the exact hinge callback types (onHingeChange / UIHingeInteraction), hinge-driven effects, pose-to-pattern mapping (companion pane, dual view, list-detail, tabletop controls), two-pane paywalls and fold-aware onboarding. Use when sizing a layout or asset for iPhone Duo, wiring a hinge callback, choosing what goes in each pane, or designing onboarding and purchase flows that span the fold.
How do I install this agent skill?
npx skills add https://github.com/mirzaaghazadeh/iphone-duo-skills --skill iphone-duo-dual-pane-patternsIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill is a technical guide for designing and developing dual-pane applications for the iPhone Duo device. It contains legitimate UI code snippets, device dimensions, and design patterns. No security risks were identified.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
What does this agent skill do?
Dual-pane patterns, dimensions and hinge callbacks
The sibling skills explain the platform: reserved regions and arrangements in
../iphone-duo-adaptive-layout/SKILL.md, the hinge and scenes in
../iphone-duo-hinge-and-scenes/SKILL.md, design principles in
../iphone-duo-design-review/SKILL.md. This one is the applied layer: the
numbers you'll want when mocking up or sizing assets, the exact shapes of the
hinge callbacks, and the proven patterns for what to put in each pane — drawn
from Apple's material plus a decade of dual-screen guidance from Microsoft
(Surface Duo), Google (Android foldables) and Samsung (Flex Mode), which all
converge on the same answers.
The numbers
From Apple's published specs. Use them for mockups, asset budgets and sanity checks — never as layout constants. Layout reads size class, safe area and reserved regions at runtime.
| Outer display | Inner display (open) | Each half of the inner display | |
|---|---|---|---|
| Diagonal | 5.4 in | 7.6 in | — |
| Points (w × h, as held) | 466 × 678 portrait | 951 × 669 landscape | 475.5 × 669 portrait |
| Pixels (w × h, as held) | 1398 × 2034 portrait | 2670 × 1878 landscape | ≈1335 × 1878 portrait |
| Density | 460 ppi | 430 ppi | 430 ppi |
| Aspect ratio | ≈1.45 : 1 (tall) | ≈1.42 : 1 (wide) | ≈1.41 : 1 (tall) |
| Size class | compact × regular (portrait), compact × compact (landscape) | regular × regular, any orientation | — |
Apple lists the inner panel as "1878-by-2670"; open, the device is wider than tall (164.6 × 117.8 mm), so the natural open posture is landscape.
Three consequences worth internalising:
- The inner display is the outer display turned sideways, near enough. Both are ≈1.4–1.45 : 1, which is why Apple describes them as sharing one aspect ratio.
- Each half of the inner display is roughly one outer display. A phone-shaped UI fits one half; the other half is additional. That is the whole design thesis: one familiar pane plus one extra pane, not one stretched phone UI.
- The fold runs down the middle of the long edge when open, so default two-pane layouts are side by side, and tabletop layouts are top/bottom.
Points
Apple's Tech Specs list pixels only. The point sizes come from the iPhone Duo templates in Apple's iOS and iPadOS 27 design resource (as reproduced in Noah Elhadedy's community iPhone Duo Adaptive Design Starter Kit, aligned to Apple's templates on 23 September 2026):
- Outer: 466 × 678 pt — exactly the 1398 × 2034 panel at 3×.
- Inner: 951 × 669 pt (669 × 951 in portrait). That is not an exact 3× of the 2670 × 1878 panel: the system renders at 2853 × 2007 (the App Store Connect screenshot size) and downsamples, like iPhone 6 Plus did. Export inner-display assets at 3× of the point size, not from the panel pixels.
- The fold sits at 475.5 pt from the edge — the centre of 951 — in both orientations.
Still confirm in the Xcode 27.1 simulator; read GeometryProxy.size or scene
bounds rather than hardcoding any of this.
Layout tokens (pt)
For mockups and for sanity-checking what the system gives you at runtime. The Source column matters: only Apple values come from Apple's design resource; kit values are design choices from the community starter kit that you're free to change.
Safe areas — asymmetric by design; never mirror one side onto the other.
| State | Top | Bottom | Leading | Trailing | Safe content size | Source |
|---|---|---|---|---|---|---|
| Outer portrait | 0 | 0 | 0 | 84 (vertical bar) | 382 × 678 | Apple |
| Outer landscape | 0 | 0 | 0 | 84, on the camera edge | 594 × 466 | Apple |
| Inner landscape (flat or book) | 0 | 0 | 0 | 84 (vertical bar) | 867 × 669 | Apple |
| Inner portrait (flat or tabletop) | 84 (status + top bar) | 95 (tab bar) | 0 | 0 | 669 × 772 | Apple |
Horizontal bars return only on the inner display in portrait. Everywhere else, navigation, toolbar and tab bar share one 84 pt vertical bar in the order back → prominent action → toolbar groups → tab bar, top to bottom.
Bars and controls
| Element | Size | Source |
|---|---|---|
| Vertical bar width | 84 pt | Apple |
| Horizontal navigation bar / toolbar (inner portrait) | 48 pt tall | Apple |
| Horizontal tab bar (inner portrait) | 95 pt tall inset | Apple |
| Bar button / prominent bar button | 36 pt / 48 pt | Apple |
| Touch target | 44 × 44 pt default, 28 × 28 pt minimum | Apple HIG |
Margins, gaps, fold
| Token | Value | Source |
|---|---|---|
| Layout margin, both displays | 20 pt | Apple |
| Division region width when flat | 0 pt | Apple |
| Division region width when folded | not published — read ReservedRegion.frame | — |
| Fold avoidance band (mockups) | 27 pt, centred on 475.5 pt | kit |
| Gap between panes | 16 pt outer, 24 pt inner | kit |
| Max width for running text | 600 pt | kit |
Columns — Apple asks for an even count; the counts themselves are kit values.
| State | Columns | Gutter |
|---|---|---|
| Outer portrait | 4 | 16 |
| Outer landscape | 6 | 16 |
| Inner portrait | 6 | 24 |
| Inner landscape | 8 | 24 |
| Inner, book-folded | 4 + 4, meeting at the fold, none on it | 24 |
Typical pane widths inside the 867 pt inner-landscape safe area (kit): list 300–320 + detail 546–566; sidebar 280; player/primary ≈462 (one half) + context ≈404. Keep a pane's content close to its outer-display size rather than stretching it.
Physical
| Open | Closed | |
|---|---|---|
| Width × height | 164.6 × 117.8 mm | 84.1 × 117.8 mm |
| Thickness | 5.2 mm | 11.3 mm |
| Weight | 254 g | 254 g |
Biometrics are Touch ID in the side button — no Face ID. Any flow that says
"Face ID" or uses a face glyph needs to branch on LAContext.biometryType.
No LiDAR is listed: features built on RoomPlan or scene depth need a
fallback on this device.
Hinge callbacks — exact shapes
All iOS 27.1 (beta at time of writing), verified against Apple's symbol docs.
SwiftUI
nonisolated func onHingeChange(
isEnabled: Bool = true,
_ action: @escaping (DeviceHingeContext, DeviceHingeContext) -> Void
) -> some View
| Type | Member | Type of member | Notes |
|---|---|---|---|
DeviceHingeContext | hinge | DeviceHinge? | nil → the device has no hinge |
DeviceHinge | angle | Angle | use .degrees / .radians |
DeviceHinge | status | DeviceHinge.Status | |
DeviceHinge.Status | .closed, .partiallyOpen, .fullyOpen | struct with static members | Equatable, so switch works, but you need a default |
The closure receives (old, new). Use isEnabled: to stop updates when an
effect is off-screen instead of ignoring them in the closure.
UIKit
init(updateHandler: (UIHingeInteraction, UIHingeInteraction.Update) -> Void)
var isEnabled: Bool
| Type | Member | Type of member | Notes |
|---|---|---|---|
UIHingeInteraction.Update | hinge | UIHinge? | nil when the view leaves a hierarchy that provides hinge updates |
UIHinge | angle | CGFloat | radians, not degrees |
UIHinge | status | UIHinge.Status | |
UIHinge.Status | .closed, .partiallyOpen, .fullyOpen, .unknown | enum (Int raw value) | UIKit has a fourth case SwiftUI doesn't |
The UIKit handler is not an (old, new) pair. It fires when the hinge
changes and when the interaction moves between hierarchies. Keep your own
previous value if you need a delta.
Rules Apple states in the reference
- Status is decided by the system from angle and device orientation. Don't re-derive it from angle thresholds — there is no documented "closed below N°".
- Update rate and precision are system policy and can change with system state. Don't assume a frequency; interpolate or animate towards the latest value rather than treating each callback as a frame.
- If you only need closed / partial / open, use
status, notangle. - The angle range isn't documented. Don't assume flat is exactly 180° — treat
.fullyOpenas your upper bound and normalise against it.
A reusable normaliser
/// 0 = closed, 1 = fully open. Effects read this; nothing lays out from it.
@Observable final class HingeProgress {
var value: Double = 1 // no hinge → behave as if flat
var isPartial = false
private var maxSeen: Double = .pi
func update(_ hinge: DeviceHinge?) {
guard let hinge else { value = 1; isPartial = false; return }
let radians = hinge.angle.radians
switch hinge.status {
case .closed:
value = 0; isPartial = false
case .fullyOpen:
maxSeen = max(maxSeen, radians)
value = 1; isPartial = false
default: // .partiallyOpen
value = min(max(radians / maxSeen, 0), 1); isPartial = true
}
}
}
// usage
.onHingeChange(isEnabled: effectVisible) { _, new in progress.update(new.hinge) }
The UIKit equivalent reads update.hinge?.angle (already radians) and must add a
case .unknown: that leaves the last value untouched.
Pose → pattern
There is no "laptop" or "tent" status. Poses are what the user does; your code sees size class, reserved regions and hinge status. Map them like this:
| Pose | What your code observes | Pattern |
|---|---|---|
| Closed | outer display, compact width | The phone app you already have. Secondary pane collapses to a sheet or a push. |
| Open flat | regular × regular, division region inactive (zero width) | Two panes side by side, media may cross the centre. |
| Book (partly folded, inner landscape) | division region active and taller than wide | Two panes, left/right. Nothing interactive in the curve. |
| Tabletop / laptop (partly folded, inner portrait) | division region active and wider than tall | Content in the raised half, controls in the flat half. |
| Tent | partially open, often flipped | Hands-free viewing; don't rely on touch. |
Detect orientation of the fold from the region, not the hinge:
GeometryReader { proxy in
let fold = proxy.reservedRegions(kind: .division).first { $0.isActive }
let isTabletop = fold.map { $0.frame.width > $0.frame.height } ?? false
// …
}
Better still, let ArrangementView with .split choose the axis for you — it
splits horizontally when wider than tall and vertically when taller than wide,
which is the book-vs-tabletop decision.
The five patterns that work
Microsoft's dual-screen taxonomy, reconciled with Apple's containers:
| Pattern | Left / top pane | Right / bottom pane | Apple container | Good for |
|---|---|---|---|---|
| Companion pane | Canvas (photo, room, document) | Tools, library, properties, prompt | ArrangementView(.split) | Editors, creative tools |
| Dual view | Version A (original) | Version B (result) | Two columns on regular width | Before/after, diffs, compare variants |
| List-detail | List | Detail, updates on tap | NavigationSplitView | Browsing catalogs, styles, messages |
| Extended canvas | One image spanning both panes | — | full-bleed + reserved regions for overlays | Maps, panoramas, video |
| Tabletop controls | Media, no buttons | Joystick, sliders, actions | ArrangementView(.split) / .overlay | Hands-free viewing, showroom, playback |
Pick by content relationship, not by pose: companion = one thing + its tools; dual view = two versions of one thing; list-detail = many things + one.
Six rules that come from all four vendors
- Mask media, split controls. Photos and video may run across the fold. Buttons, text and inputs never sit on it. The only exception is a drag in progress — a dragged card may cross the fold while the finger is down.
- Don't stretch. Each half is phone-shaped. Doubling font size or button width across 2670 px is the canonical failure. Use an even column count so nothing straddles the centre.
- Tabletop: content up, controls down. The raised half is for looking, the flat half is for touching. Nothing interactive near the fold.
- Tools go right (or bottom) of the canvas — thumb side for most users, and where iOS 27.1 already puts the vertical bar.
- Two screens = compare. If the app has any before/after or variant choice, the fold is the natural divider; a single-screen slider is no longer needed.
- State survives the fold. Opening and closing resizes the scene; it isn't
torn down. Scroll position, in-progress text and the current step live in
the model, not in view
@Statethat a layout swap discards. Dialogs stay within one half.
Hinge-driven effects
The hinge is for effects and interactions, never layout. Effects that earn their place:
- Hinge as a handle. Map normalised progress to a reveal — a door swinging
open onto a scene, a cover lifting, a camera pushing in. The user's hand is
the animation. Keep it an overlay under a second long, never blocking input,
and on
.fullyOpenfinish the transition and remove the overlay. - Close to save. On
.closed, persist and show a short confirmation on the outer display. Pairs naturally with the reveal running in reverse. - Parallax pitch. Combine CoreMotion tilt (yaw/roll) with hinge progress (pitch) to move 2.5D layers — a depth map split into 3–4 layers is enough. Clamp amplitude to avoid motion sickness.
- Continuous controls. Anything a lever or whammy bar could drive.
Always:
- Guard
nilhinge (most devices) and give those users the end state. - Reset in the
elsebranch when leaving.partiallyOpen, so effects don't stick at a stale angle. - Honour Reduce Motion — swap the reveal for a crossfade.
- Pass
isEnabled: falsewhen the effect isn't on screen.
Purchase and paywall screens
- Sheets avoid the fold on their own, so a sheet paywall lands in one half. To span both panes, present a full-screen cover with your own two-column layout: value proposition (ideally the user's own result, animated) in the left pane, plans and the purchase button entirely in the right pane.
- Price, button and legal copy stay together in one pane. Never split the price from the button across the fold, and never run the button through it.
- On the outer display, fall back to one column;
SubscriptionStoreViewhandles trial badges and renewal wording for you there. - App Review 3.1.2 still governs: the billed amount is the most prominent element, trial wording is secondary, the renewal period is explicit, restore purchases is reachable, and the close button is visible immediately.
- Folding is not a dismissal. Don't treat a close/open during a paywall as "user declined" for exit-offer logic; the scene was only resized.
- Tabletop-triggered paywalls should keep the tabletop shape: preview up top, plans on the flat half.
Fold-aware onboarding
- Never require opening the device. Invite it — "open to step inside" — and continue on the outer display after a few seconds if the user doesn't. People hold Duo closed a lot.
- Opening at any step expands that step into two panes. It never restarts the flow. This is the state-survives-the-fold rule applied to onboarding.
- Reach the "aha" before asking for permissions or money. Permission priming screens go directly before the system prompt, at the moment of need, with a "not now".
- Ask for notifications after the paywall, attached to a concrete promise (for example, "we'll remind you before your trial ends").
Test matrix
Run every two-pane screen through this in the Xcode 27.1 Device Hub simulator, dragging the hinge slider slowly through each transition:
| Closed | Book (slider mid-way) | Open flat | Tabletop | Split View, left | Split View, right | |
|---|---|---|---|---|---|---|
| No letterboxing | ☐ | ☐ | ☐ | ☐ | ☐ | ☐ |
| Nothing tappable in the fold | — | ☐ | — | ☐ | — | — |
| State kept across the transition | ☐ | ☐ | ☐ | ☐ | ☐ | ☐ |
| Hinge effect resets / no-hinge path works | ☐ | ☐ | ☐ | ☐ | — | — |
| Paywall price + button in one pane | ☐ | ☐ | ☐ | ☐ | ☐ | ☐ |
Also run once on a non-foldable simulator to exercise the nil-hinge path.
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/mirzaaghazadeh/iphone-duo-skills/iphone-duo-dual-pane-patterns">View iphone-duo-dual-pane-patterns on skillZs</a>