iPhone Duo 上の Expo
出典: https://expo.dev/blog/supporting-iphone-duo (Expo SDK 58)。デザインルールは apple-design-duo にあります。このスキルは Expo および React Native の実装レイヤーです。CI パイプラインの背景: https://docs.expo.dev/tutorial/cicd/introduction/ および expo:eas-workflows スキルを参照してください。
要件
- Expo SDK 58 (iOS 27 をターゲット) および Xcode 27.1 (iOS 27.1 ランタイムと iPhone Duo シミュレータを含む)。
- EAS Build イメージ:
sdk-58またはlatest。 - Xcode 27 は Simulator.app を Device Hub に置き換えます (Xcode > Open Developer Tool > Device Hub)。折りたたみ、回転、ヒンジ角度設定が可能な 3D Duo を備えています。
メンタルモデル
折りたたみ = コンパクト幅、開いた状態 = 通常幅。レイアウトを広げるだけで、デザインを切り替えない。両方の状態で同じ機能と状態を維持。本のような姿勢では、iOS がステータスバー、タブバー、ツールバーを一方の端の垂直レールに移動します。
4つのルール
- iOS にナビゲーションを描画させる。 Expo Router の
StackとNativeTabsを使用してください。カスタム JS ヘッダーと<TabList>はレールに対応しておらず(SDK 58 では最小限の互換性のみ追加)、<TabList>はレール内に配置できません。 - すべての画面で
SafeAreaView(react-native-safe-area-context) を使用し、水平方向のインセットも含める。 - すべての姿勢をテストする。 折りたたみ、展開、回転、Split View。
- 必要な箇所のみを調整する:
ArrangementView、preferredPlacement、予約領域、ヒンジ API、またはレールの無効化。
セーフエリア(ほとんどのバグが発生する場所)
- レールは左または右に 84pt のインセット が必要です。
- スクロールコンテンツは
SafeAreaView edges={['left','right']}でラップし、ScrollView自体にはラップしないでください(背景を端から端まで表示するため)。ScrollViewにcontentInsetAdjustmentBehavior="automatic"を設定してください(RN パッチ #59194 により、これだけで十分になるはずです)。 - 水平カルーセル: 自身で端にパディングを追加します。
- フローティングボタンとトースト:
right: insets.right + 16。RTL では、leftとrightの両方を読み取ります。 NativeTabs内では、insets.bottomはタブバーが下部にある場合のみその高さを含みます。固定のタブバーの高さを追加しないでください。- ルートビューではなく、コンテンツコンテナにインセットを適用します。
useWindowDimensionsをレンダリング時に読み取ります。左側の Split View ウィンドウの幅は 469pt です。
Expo Router
- レール内のヘッダーボタンは 1 列に積み重ねられ、Back または Close が最初に来ます。テキストのみのアイテムは水平のままです。カスタムビューは除外されます。
- Dynamic Island の Live Activity により、ツールバーがオーバーフローして縮小される可能性があります。これに対抗しないでください。
<SplitView>はシステムの Split View をラップします: 折りたたみ時は 1 列、開いた時は 2 列、折りたたみを認識します。SDK 58 では実験的機能です。NativeTabsのpreferredPlacement:'sidebar' | 'tabBar' | 'automatic'。ランドスケープで開いているディスプレイに適用されます。
追加 API
| ニーズ | API |
|---|---|
| 2ペインコンテンツ (iOS 27.1 のみ、古いバージョンでは子要素がコンテナなしでレンダリングされます) | @expo/ui/swift-ui の ArrangementView、Host 内で使用し、ArrangementView.Primary と .Secondary を配置。`arrangementViewStyle('split' |
| 折り目と隠蔽フレーム | npx expo install react-native-reserved-regions、その後 useReservedRegions() で `{ kind: 'division' |
| ヒンジ角度と状態 | npx expo install expo-hinge。useHinge()、getHinge()、addHingeListener()。状態: closed、partiallyOpen、fullyOpen、unknown。SwiftUI の Modifier onHingeChange は useWorkletCallback を介して UI スレッドで実行可能。Reanimated および Android 11+ の折りたたみデバイスでも動作します。 |
| レールの端 | useVerticalBarEdgeIOS() は 'leading'、'trailing'、または null を返します。Duo 検出器ではありません(開いたポートレートでは null を返します)。 |
| レールの無効化 | <StatusBar verticalBarBehaviorIOS="disabled" /> (preferredVerticalBarBehavior にマッピング)。動画プレーヤーや電卓スタイルの画面でのみ使用し、画面ごとに設定を安定させてください。 |
画面回転
iOS 27 では、画面回転ロックはリサイズ可能なコンテキストにおけるプリファレンスです。lockAsync は拒否される可能性があり、requireFullScreen はもはやオプトアウトしません。Duo でポートレートまたはランドスケープにロックするには、姿勢変更時にアプリが画面を埋める場合にのみ機能します。
既知の制限
- StandBy は Duo シミュレータでは利用できず、ほとんどのアプリ拡張機能は Xcode 27.1 のシミュレータでは利用できません。ウィジェットと拡張機能は実機でテストしてください。
expo-app-intents(アルファ版、SDK 58 も対象)は、Siri、Shortcuts、Spotlight にアクションを公開します。一部のスキーマは、そのドメインよりも新しい iOS を必要とします(.mail.openDraftは iOS 27 が必要)。レイアウトとは無関係です。
テストチェックリスト
- 折りたたみ時: レールの下に重要なものがないこと、ボタンが反応すること。
- 開いた時: レイアウトがデザインを切り替えないこと、レールの下に何もないこと、ポートレートのバーが上部と下部に戻ること。
- 半開き時: 折り目に重要なものがなく、シートとポップオーバーが片側に収まること。
- Split View 時: コンテンツがレールから離れていること(左ウィンドウの左端)。
- 画面中央で折りたたみ中: スクロール位置、状態、アクションが保持されること。
- 標準的な iPhone 時: 何も変わらないこと。
監査ワークフロー
- SDK 58 を確認し、再ビルドする。
- カスタムヘッダーまたはタブバー (
headerShown: falseと手動構築バー、TabList) を検索し、StackとNativeTabsに移行する。 - ハードコードされた
top、bottom、left、rightのオフセット、固定のタブバーの高さ、ルートレベルのパディングを検索し、インセットに置き換える。 - カルーセル、FAB、トーストをセーフエリアのリストに従って確認する。
- 2ペイン画面を選択:
SplitViewまたはArrangementView。 - 画面ごとに、カスタムレイアウトで本当に必要な場合にのみ、予約領域またはヒンジを使用するか判断する。
- Device Hub で検証し、その後 CI で検証する(以下参照)。
CI: Duo シミュレータで実行する
expo:eas-workflows スキルを使用してください(最初にライブスキーマを取得し、ジョブパラメータを推測しないでください)。CI/CD チュートリアルから適切な形状: PR トリガーのシミュレータビルドを sdk-58 または latest イメージ上で行い、オプションで Maestro E2E を実行し、指紋ベースでネイティブビルドと本番用の EAS Update を選択します。npx -y eas-cli@latest workflow:validate .eas/workflows/<file>.yml --non-interactive で検証してください。Workflows は有料の EAS 分を消費します。
よくある間違い
| 間違い | 修正 |
|---|---|
| ScrollView にインセットをパディングする | その周りを SafeAreaView で囲み、contentInsetAdjustmentBehavior="automatic" を設定 |
| パディングに固定のタブバーの高さを使用する | セーフエリアから insets.bottom を使用 |
useVerticalBarEdgeIOS() を Duo の検出に使用する | これはレールの端のみを示し、開いたポートレートでは null |
| 予約領域で折り目にコンテンツを分割する | ArrangementView または SplitView を使用 |
| 手動で構築した JS ヘッダーまたはタブバー | Stack と NativeTabs を使用 |
right: 16 のフローティングボタン | right: insets.right + 16 に変更 |
lockAsync が常に成功すると仮定する | プリファレンスとして扱い、画面を埋める |
| 標準的な iPhone でのみテストする | Device Hub ですべての姿勢をテスト |