iphone-duo-dual-pane-patterns について
iPhone Duoのデュアルペイン画面を開発するための実践的な設計スキルです。端末寸法、ヒンジコールバック、ペインへのコンテンツ配置パターンを解説します。
主な機能
- 01端末寸法とレイアウトトークン
- 02SwiftUIとUIKitのヒンジコールバック
- 03端末の姿勢をレイアウトパターンに対応
- 045種類のデュアルペインパターン
活用シーン
- 01画面モックやアセットの寸法決定
- 02ヒンジ状態の変化への対応
- 03エディターや比較画面のペイン選定
- 04卓上姿勢でのメディアと操作部の配置
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mirzaaghazadeh/ iphone-duo-skills --skill iphone-duo-dual-pane-patternsインストール後、Claude Code で/iphone-duo-dual-pane-patternsを入力して読み込まれたか確認します。
詳細
2画面レイアウトのパターン、寸法、ヒンジコールバック
関連するスキルでは、プラットフォームについて説明しています。予約領域と配置については
../iphone-duo-adaptive-layout/SKILL.md、ヒンジとシーンについては
../iphone-duo-hinge-and-scenes/SKILL.md、設計原則については
../iphone-duo-design-review/SKILL.mdを参照してください。このスキルは実践編です。モックアップやアセットのサイズ調整に役立つ数値、ヒンジコールバックの正確な形式、各ペインに何を配置するかについて実証済みのパターンを紹介します。Appleの資料に加え、Microsoft(Surface Duo)、Google(Androidの折りたたみ式デバイス)、Samsung(Flex Mode)による10年分のデュアルスクリーン設計ガイダンスをもとにしており、いずれも同じ答えに行き着いています。
数値
Appleが公開している仕様に基づきます。モックアップ、アセットの予算、妥当性の確認に使用してください。ただし、レイアウト定数としては決して使用しないでください。レイアウトは、実行時にサイズクラス、セーフエリア、予約領域を読み取ります。
| 外側ディスプレイ | 内側ディスプレイ(開いた状態) | 内側ディスプレイの各半分 | |
|---|---|---|---|
| 対角線 | 5.4 in | 7.6 in | — |
| ポイント(幅 × 高さ、通常の向き) | 466 × 678 ポートレート | 951 × 669 ランドスケープ | 475.5 × 669 ポートレート |
| ピクセル(幅 × 高さ、通常の向き) | 1398 × 2034 ポートレート | 2670 × 1878 ランドスケープ | ≈1335 × 1878 ポートレート |
| 密度 | 460 ppi | 430 ppi | 430 ppi |
| アスペクト比 | ≈1.45 : 1(縦長) | ≈1.42 : 1(横長) | ≈1.41 : 1(縦長) |
| サイズクラス | compact × regular(ポートレート)、compact × compact(ランドスケープ) | regular × regular、すべての向き | — |
Appleの仕様では内側のパネルを「1878-by-2670」と記載しています。デバイスを開くと縦より横のほうが長く(164.6 × 117.8 mm)、自然な開いた状態の向きはランドスケープです。
覚えておくべき点は3つあります。
- 内側ディスプレイは、ほぼ外側ディスプレイを横向きにしたものです。 どちらも約1.4〜1.45 : 1であるため、Appleは同じアスペクト比を共有すると説明しています。
- 内側ディスプレイの各半分は、おおよそ外側ディスプレイ1つ分です。 スマートフォン向けのUIが片方の半分に収まり、もう片方は追加の領域になります。これが設計の要点です。使い慣れたペインにもう1つのペインを加えるのであり、スマートフォン向けUIを横に引き伸ばすのではありません。
- 開いた状態では、折り目は長辺の中央を縦に走ります。 そのため、デフォルトの2ペインレイアウトは左右に並び、テーブルトップレイアウトは上下に並びます。
ポイント
AppleのTech Specsに記載されているのはピクセル数のみです。ポイントサイズは、AppleのiOS and iPadOS 27 design resourceに含まれるiPhone Duoテンプレートから取得しています(Noah Elhadedyによるコミュニティ版iPhone Duo Adaptive Design Starter Kitで再現。2026年9月23日時点でAppleのテンプレートと整合しています)。
- 外側:466 × 678 pt — 1398 × 2034のパネルと正確に3倍の関係です。
- 内側:951 × 669 pt(ポートレートでは669 × 951)。2670 × 1878のパネルの正確な3倍ではありません。iPhone 6 Plusと同様に、システムは2853 × 2007(App Store Connectのスクリーンショットサイズ)でレンダリングしてからダウンスケールします。内側ディスプレイ用のアセットは、パネルのピクセル数ではなく、ポイントサイズの3倍で書き出してください。
- 折り目は、どちらの向きでも端から475.5 ptの位置、つまり951の中央にあります。
それでも、Xcode 27.1シミュレーターで確認してください。これらの値をハードコードするのではなく、GeometryProxy.sizeまたはシーンの境界を読み取ってください。
レイアウトトークン(pt)
モックアップの作成や、実行時にシステムから取得できる値の確認に使用します。Source列は重要です。Appleの値はAppleのデザインリソースに由来するものだけで、kitの値はコミュニティのスターターキットによるデザイン上の選択であり、自由に変更できます。
セーフエリア — 非対称なのは意図的な設計です。一方の値をもう一方に反映させないでください。
| 状態 | 上 | 下 | 左端 | 右端 | セーフコンテンツサイズ | ソース |
|---|---|---|---|---|---|---|
| 外側ディスプレイの縦向き | 0 | 0 | 0 | 84(縦バー) | 382 × 678 | Apple |
| 外側ディスプレイの横向き | 0 | 0 | 0 | 84(カメラ側の端) | 594 × 466 | Apple |
| 内側ディスプレイの横向き(フラットまたはブック) | 0 | 0 | 0 | 84(縦バー) | 867 × 669 | Apple |
| 内側ディスプレイの縦向き(フラットまたはテーブルトップ) | 84(ステータスバー+上部バー) | 95(タブバー) | 0 | 0 | 669 × 772 | Apple |
横向きのバーが表示されるのは、内側ディスプレイを縦向きにした場合のみです。それ以外の状態では、ナビゲーション、ツールバー、タブバーは、上から下へ、戻る → 主要アクション → ツールバーグループ → タブバーの順に、1つの84 ptの縦バーを共有します。
バーとコントロール
| 要素 | サイズ | ソース |
|---|---|---|
| 縦バーの幅 | 84 pt | Apple |
| 横向きのナビゲーションバー/ツールバー(内側ディスプレイの縦向き) | 高さ48 pt | Apple |
| 横向きのタブバー(内側ディスプレイの縦向き) | インセット込みで高さ95 pt | Apple |
| バーボタン/主要バーボタン | 36 pt/48 pt | Apple |
| タッチターゲット | デフォルト44 × 44 pt、最小28 × 28 pt | Apple HIG |
マージン、間隔、折り目
| トークン | 値 | ソース |
|---|---|---|
| 両ディスプレイのレイアウトマージン | 20 pt | Apple |
| フラット時の分割領域の幅 | 0 pt | Apple |
| 折りたたみ時の分割領域の幅 | 未公開 — ReservedRegion.frameを読み取る | — |
| 折り目回避帯(モックアップ) | 27 pt、475.5 ptを中心とする | kit |
| ペイン間の間隔 | 外側16 pt、内側24 pt | kit |
| 本文の最大幅 | 600 pt | kit |
列 — Appleは偶数列を推奨しています。列数そのものはkitの値です。
| 状態 | 列数 | ガター |
|---|---|---|
| 外側ディスプレイの縦向き | 4 | 16 |
| 外側ディスプレイの横向き | 6 | 16 |
| 内側ディスプレイの縦向き | 6 | 24 |
| 内側ディスプレイの横向き | 8 | 24 |
| 内側、ブック型に折りたたみ | 4 + 4、折り目で接し、折り目上には列を置かない | 24 |
内側ディスプレイの横向きのセーフエリア867 pt内での一般的なペイン幅(kit):リスト300~320 + 詳細546~566、サイドバー280、プレーヤー/メイン≈462(片側半分)+ コンテキスト≈404。ペインを引き伸ばすのではなく、そのコンテンツ幅を外側ディスプレイのサイズに近づけてください。
物理仕様
| 開いた状態 | 閉じた状態 | |
|---|---|---|
| 幅 × 高さ | 164.6 × 117.8 mm | 84.1 × 117.8 mm |
| 厚さ | 5.2 mm | 11.3 mm |
| 重量 | 254 g | 254 g |
生体認証はサイドボタンのTouch IDで、Face IDはありません。「Face ID」と表示するフローや顔のグリフを使うフローでは、LAContext.biometryTypeに応じて分岐させてください。このデバイスにはLiDARが搭載されていません。RoomPlanやシーン深度を利用する機能には、このデバイスでのフォールバックが必要です。
ヒンジコールバック — 正確な形式
執筆時点でiOS 27.1(ベータ版)のすべての内容を、Appleのシンボルドキュメントと照合して確認済みです。
SwiftUI
nonisolated func onHingeChange(
isEnabled: Bool = true,
_ action: @escaping (DeviceHingeContext, DeviceHingeContext) -> Void
) -> some View
| 型 | メンバー | メンバーの型 | 注記 |
|---|---|---|---|
DeviceHingeContext | hinge | DeviceHinge? | nil → デバイスにヒンジがありません |
DeviceHinge | angle | Angle | .degrees/.radiansを使用 |
DeviceHinge | status | DeviceHinge.Status | |
DeviceHinge.Status | .closed、.partiallyOpen、.fullyOpen | 静的メンバーを持つ構造体 | Equatableなのでswitchが使えますが、defaultが必要です |
クロージャには(old, new)が渡されます。エフェクトが画面外にあるときは、クロージャ内で更新を無視するのではなく、isEnabled:を使って更新を停止してください。
UIKit
init(updateHandler: (UIHingeInteraction, UIHingeInteraction.Update) -> Void)
var isEnabled: Bool
| 種類 | メンバー | メンバーの型 | 備考 |
|---|---|---|---|
UIHingeInteraction.Update | hinge | UIHinge? | ヒンジ更新を提供する階層からビューが離れると nil になります |
UIHinge | angle | CGFloat | 度ではなくラジアンです |
UIHinge | status | UIHinge.Status | |
UIHinge.Status | .closed、.partiallyOpen、.fullyOpen、.unknown | enum(Int の raw value) | UIKit には SwiftUI にない4つ目のケースがあります |
UIKit のハンドラーは (old, new) のペアではありません。ヒンジが
変化したときと、インタラクションが階層間を移動したときに呼び出されます。差分が
必要な場合は、独自に前回の値を保持してください。
Apple がリファレンスで示すルール
- ステータスは、角度とデバイスの向きの両方からシステムが決定します。 角度のしきい値から再計算しないでください。「N°未満なら閉じている」といった説明はありません。
- 更新頻度と精度はシステムのポリシーであり、システムの状態によって変わることがあります。頻度を決めつけず、各コールバックを1フレームとして扱うのではなく、最新の値に向けて補間またはアニメーションしてください。
- 閉じている/部分的に開いている/開いている状態だけが必要な場合は、
angleではなくstatusを使用してください。 - 角度の範囲は文書化されていません。 完全に開いた状態が厳密に180°だと決めつけず、
.fullyOpenを上限として正規化してください。
再利用可能な正規化関数
/// 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) }
UIKit で同等の処理をする場合は update.hinge?.angle(すでにラジアン)を読み取り、
最後の値をそのまま維持する case .unknown: を追加する必要があります。
ポーズ → パターン
「ノートパソコン」や「テント」というステータスはありません。ポーズはユーザーがとる姿勢であり、コードから見えるのは サイズクラス、予約領域、ヒンジのステータスです。次のように対応付けてください。
| ポーズ | コードから観測できること | パターン |
|---|---|---|
| 閉じた状態 | 外側のディスプレイ、コンパクト幅 | 既存のスマートフォンアプリです。セカンダリペインはシートまたはプッシュに折りたたまれます。 |
| 完全に開いた状態 | regular × regular、分割領域は非アクティブ(.includeInactive を指定した場合のみ返されます) | 2つのペインを横並びにし、メディアを中央の境界をまたいで表示できます。 |
| ブック(部分的に折りたたみ、内側は横向き) | 分割領域が アクティブで、幅より高さがあります | 左右に2つのペインを配置します。曲面にはインタラクティブな要素を置きません。 |
| テーブルトップ/ノートパソコン(部分的に折りたたみ、内側は縦向き) | 分割領域が アクティブで、高さより幅があります | 起き上がっている側にコンテンツを、平らな側にコントロールを配置します。 |
| テント | 部分的に開いており、反転していることがよくあります | ハンズフリーでの視聴に適しています。タッチ操作を前提にしないでください。 |
ヒンジではなく、領域から折り目の向きを検出してください。
GeometryReader { proxy in
let fold = proxy.reservedRegions(kind: .division).first { $0.isActive }
let isTabletop = fold.map { $0.frame.width > $0.frame.height } ?? false
// …
}
さらによい方法は、.split を指定した ArrangementView に軸を選ばせることです。幅が高さより大きい場合は水平方向に、高さが幅より大きい場合は垂直方向に分割します。これはまさにブックとテーブルトップを判別する方法です。
効果的な5つのパターン
Microsoft のデュアルスクリーンの分類を Apple のコンテナと照らし合わせたものです。
| パターン | 左/上のペイン | 右/下のペイン | Apple のコンテナ | 適した用途 |
|---|---|---|---|---|
| コンパニオンペイン | キャンバス(写真、部屋、ドキュメント) | ツール、ライブラリ、プロパティ、プロンプト | ArrangementView(.split) | エディター、クリエイティブツール |
| デュアルビュー | バージョンA(オリジナル) | バージョンB(結果) | レギュラー幅での2カラム | ビフォーアフター、差分、バリエーションの比較 |
| リストと詳細 | リスト | タップで更新される詳細 | NavigationSplitView | カタログ、スタイル、メッセージの閲覧 |
| 拡張キャンバス | 2つのペインにまたがる1つの画像 | — | 全面表示+オーバーレイ用の予約領域 | マップ、パノラマ、動画 |
| テーブルトップコントロール | メディア、ボタンなし | ジョイスティック、スライダー、アクション | ArrangementView(.split) / .overlay | ハンズフリーでの視聴、ショールーム、再生 |
ポーズではなく、コンテンツ間の関係に基づいて選んでください。コンパニオンは1つの対象とそのツール、デュアルビューは1つの対象の2つのバージョン、リストと詳細は複数の対象とそのうちの1つです。
4 社すべてから導き出される 6 つのルール
- メディアはまたいでも、操作部は分けます。 写真や動画は折り目をまたいでもかまいません。 ボタン、テキスト、入力欄は折り目にかかってはいけません。唯一の例外はドラッグ中です。指を押さえている間は、ドラッグ中のカードが折り目をまたいでもかまいません。
- 引き伸ばさないでください。 それぞれの半分はスマートフォンの形状です。2670 px 全体にわたってフォントサイズやボタンの幅を 2 倍にするのは、典型的な失敗です。 中央をまたぐ要素がないよう、列数は偶数にします。
- テーブルトップでは、コンテンツを上に、操作部を下に配置します。 持ち上がった半分は見るための領域、平らな半分は触るための領域です。折り目の近くにはインタラクティブな要素を配置しません。
- ツールはキャンバスの右側(または下側)に配置します。多くのユーザーにとって親指で操作しやすく、iOS 27.1 でもすでに縦のバーが配置される場所です。
- 画面が 2 つなら比較します。 アプリにビフォー/アフターやバリエーションの選択がある場合、折り目が自然な区切りになります。単一画面のスライダーはもう必要ありません。
- 状態は折りたたんでも維持します。 開閉するとシーンのサイズが変わりますが、破棄されるわけではありません。スクロール位置、入力中のテキスト、現在のステップはモデルに保持し、レイアウトの切り替えで破棄されるビューの
@Stateには保持しません。ダイアログは片側の半分に収めます。
ヒンジ駆動のエフェクト
ヒンジはエフェクトとインタラクションのために使い、レイアウトには使いません。効果を発揮するエフェクトは次のとおりです。
- ヒンジをハンドルとして使います。 正規化された進行状況を、シーンへと開くドア、持ち上がるカバー、前進するカメラなどのリビールにマッピングします。ユーザーの手そのものがアニメーションになります。オーバーレイとして 1 秒未満に収め、入力を妨げないようにします。また、
.fullyOpenになったらトランジションを完了し、オーバーレイを削除します。 - 閉じたら保存します。
.closedになったらデータを永続化し、外側のディスプレイに短い確認メッセージを表示します。リビールを逆再生する動きと自然に組み合わせられます。 - 視差による傾き。 CoreMotion の傾き(ヨー/ロール)とヒンジの進行状況(ピッチ)を組み合わせ、2.5D レイヤーを動かします。深度マップを 3~4 層に分ければ十分です。 モーション酔いを避けるため、振幅を制限します。
- 連続的な操作部。 レバーやワーミーバーで操作できるものなら何でも対象になります。
常に次の点を守ります。
nilのヒンジに備えます(ほとんどのデバイスが該当します)。そのようなユーザーには最終状態を表示します。.partiallyOpenを離れるときはelse分岐でリセットし、エフェクトが古い角度のまま残らないようにします。- 視差効果を減らす設定を尊重し、リビールをクロスフェードに置き換えます。
- エフェクトが画面上にない場合は
isEnabled: falseを渡します。
購入画面とペイウォール画面
- シートは折り目を自動的に避けるため、シートのペイウォールは片側の半分に表示されます。 両方のペインにまたがるように表示するには、独自の 2 列レイアウトを備えたフルスクリーンカバーを表示します。左ペインには価値提案(できればアニメーション付きの、ユーザー自身の結果)を配置し、右ペインにはプランと購入ボタンを完全に収めます。
- 価格、ボタン、法的な説明文は同じペイン内にまとめます。 価格とボタンを折り目の両側に分けてはいけません。また、ボタンを折り目にまたがらせてはいけません。
- 外側のディスプレイでは 1 列レイアウトに切り替えます。
SubscriptionStoreViewがトライアルバッジと更新に関する文言を処理します。 - App Review 3.1.2 の要件は引き続き適用されます。請求額を最も目立つ要素にし、トライアルに関する文言は二次的に表示し、更新期間を明記し、購入の復元にアクセスできるようにし、閉じるボタンをすぐに見える状態にします。
- 折りたたみは閉じる操作ではありません。 ペイウォール表示中の閉じる/開く操作を、離脱オファーのロジックで「ユーザーが購入を辞退した」と扱ってはいけません。シーンのサイズが変わっただけです。
- テーブルトップをきっかけに表示するペイウォールも、テーブルトップの形状を維持します。プレビューを上に、プランを平らな半分に配置します。
折りたたみ対応のオンボーディング
- デバイスを開くことを必須にしないでください。 「開いて中を見てみましょう」と促し、ユーザーが開かなければ数秒後に外側のディスプレイで続行します。Duo は閉じたまま使われることも多いです。
- どの段階でもデバイスを開くと、その段階が2ペインに拡張されます。フローが最初からやり直しになることはありません。これは、オンボーディングに適用される「折り目をまたいでも状態を維持する」ルールです。
- 権限や料金について尋ねる前に、「なるほど」と思える体験を届けます。権限を事前に説明する画面は、必要になったタイミングでシステムプロンプトの直前に表示し、「今はしない」を用意します。
- 通知の許可はペイウォールの後に求め、具体的な約束に結び付けます(例:「トライアル終了前にお知らせします」)。
テストマトリックス
Xcode 27.1 の Device Hub シミュレーターで、各2ペイン画面を以下の条件でテストします。ヒンジのスライダーを各切り替え状態でゆっくり動かしてください。
| 閉じた状態 | ブック(スライダー中間) | 平らに開いた状態 | テーブルトップ | Split View、左側 | Split View、右側 | |
|---|---|---|---|---|---|---|
| レターボックス表示なし | ☐ | ☐ | ☐ | ☐ | ☐ | ☐ |
| 折り目の位置にタップ可能な要素なし | — | ☐ | — | ☐ | — | — |
| 切り替え後も状態を維持 | ☐ | ☐ | ☐ | ☐ | ☐ | ☐ |
| ヒンジ効果がリセットされる/ヒンジなしの経路が動作する | ☐ | ☐ | ☐ | ☐ | — | — |
| ペイウォールの価格とボタンが同じペインにある | ☐ | ☐ | ☐ | ☐ | ☐ | ☐ |
さらに、nil-ヒンジの経路をテストするため、非折りたたみ式のシミュレーターでも1回実行します。