Skip to main content
skill CX Codex

iphone-duo-dual-pane-patterns skill — Claude Code guide

Guides two-pane UI development for iPhone Duo, covering panel dimensions and hinge callbacks. Useful when building dual-screen flows or adapting layouts to fold poses.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add mirzaaghazadeh/iphone-duo-skills --skill iphone-duo-dual-pane-patterns -a claude-code
Open on GitHub

What iphone-duo-dual-pane-patterns does

This skill provides dimensions and layout specifications for iPhone Duo. It covers the outer display at 466 × 678 pt and the inner panel at 951 × 669 pt. The inner panel defaults to landscape when unfolded. Each half roughly matches the proportions of the outer screen, so apps place a secondary panel alongside the main view rather than stretching single views across the hinge.

It includes iOS 27.1 hinge callback signatures, such as SwiftUI's onHingeChange and UIKit's UIHingeInteraction. The reference outlines safe area offsets, like the asymmetric 84 pt vertical bar, alongside column counts and margins for book or tabletop orientations.

Hardcoding display dimensions into views breaks adaptive layouts. The skill requires reading runtime scene bounds and GeometryProxy.size directly. The device also relies on Touch ID inside the side button instead of Face ID, while LiDAR hardware is absent.

What it helps with

  • size assets for the 951 × 669 pt inner display
  • wire hinge angle handlers using SwiftUI onHingeChange
  • split interfaces into companion panes across the 475.5 pt fold
  • branch authentication flows for Touch ID instead of Face ID

Example prompts for the agent

  • Configure a two-pane layout split at 475.5 pt for book posture.
  • Add an onHingeChange listener to adjust controls when folded.
  • Check the inner landscape safe area margins for navigation bars.

How to install iphone-duo-dual-pane-patterns

1Skills CLI (Vercel)

npx skills add mirzaaghazadeh/iphone-duo-skills --skill iphone-duo-dual-pane-patterns -a claude-code

2Manually: put the files in place

~/.claude/skills/iphone-duo-dual-pane-patterns/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/iphone-duo-dual-pane-patterns/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about iphone-duo-dual-pane-patterns

How do you install iphone-duo-dual-pane-patterns in Claude Code?

Run npx skills add mirzaaghazadeh/iphone-duo-skills --skill iphone-duo-dual-pane-patterns -a claude-code. The file will install to ~/.claude/skills/iphone-duo-dual-pane-patterns/SKILL.md.

What does iphone-duo-dual-pane-patterns do?

It supplies exact panel measurements, layout tokens, and hinge event callbacks for iPhone Duo dual-screen apps. Coding agents use it to structure fold-aware view hierarchies.

Which biometrics does the device use according to the skill?

The hardware uses Touch ID built into the side button, with no Face ID or LiDAR sensors. Any security or depth scanning flow must account for these fallbacks.

Source text

SKILL.md · MIT
---
name: iphone-duo-dual-pane-patterns
description: 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.
---

# 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:

1. **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.
2. **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.
3. **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

```swift
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

```swift
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`, not `angle`.**
- The **angle range isn't documented.** Don't assume flat is exactly 180° — treat
  `.fullyOpen` as your upper bound and normalise against it.

### A reusable normaliser

```swift
/// 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 (returned only with `.includeInactive`) | 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:

```swift
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

1. **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.
2. **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.
3. **Tabletop: content up, controls down.** The raised half is for looking, the
   flat half is for touching. Nothing interactive near the fold.
4. **Tools go right** (or bottom) of the canvas — thumb side for most users, and
   where iOS 27.1 already puts the vertical bar.
5. **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.
6. **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 `@State` that 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 `.fullyOpen` finish 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 `nil` hinge (most devices) and give those users the end state.
- Reset in the `else` branch when leaving `.partiallyOpen`, so effects don't
  stick at a stale angle.
- Honour **Reduce Motion** — swap the reveal for a crossfade.
- Pass `isEnabled: false` when 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; `SubscriptionStoreView`
  handles 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

1. **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.
2. **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.
3. 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".
4. 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.

© mirzaaghazadeh · MIT · mirzaaghazadeh/iphone-duo-skills

Similar skills

Everything for Mobile →

From the same set8

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor