mcpField
I
iphone-duo-adaptive-layout

iphone-duo-adaptive-layout

github.com/mirzaaghazadehデザイン・UI250704 回インストール今日更新
リポジトリ

iphone-duo-adaptive-layout について

iPhone Duo向けのインターフェース設計スキルです。折り目やカメラ、端末の姿勢に応じた予約領域、要素の移動、システム配置を解説します。

主な機能

  1. 01分割領域と遮蔽領域の取得
  2. 02折り目と端末の姿勢への対応
  3. 03コントロールや関連要素の移動
  4. 04SwiftUIとUIKitの配置機能

活用シーン

  1. 01独自コントロールを折り目から離す
  2. 02デュアルペインや卓上姿勢の画面設計
  3. 03システムの適応型コンテナを使う場面の判断

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mirzaaghazadeh/iphone-duo-skills --skill iphone-duo-adaptive-layout

インストール後、Claude Code で/iphone-duo-adaptive-layoutを入力して読み込まれたか確認します。

詳細

折り目を考慮したアダプティブレイアウト

iPhone Duo を途中まで折りたたむと、内側のディスプレイの中央がカーブします。 そのカーブ部分にあるものは読みにくくなり、タップするのもかなり難しくなります。 本の背の部分にまたがって印刷された写真でも同じことが起こります。綴じ目の近くでは、ひと続きの画像として見えなくなります。

つまり、折り目は単なる飾りではありません。コンテンツを配置できる場所に実際の制約をもたらします。iOS ではこれを、クエリしてレイアウト設計に反映できる予約領域としてモデル化しています。

予約領域

予約領域とは、ハードウェアによってレイアウトに制約が課されるビューの領域です。iPadOS で普段から考慮しているウインドウコントロールとまったく同じように扱ってください。障害物ではなく、利用可能なスペースに関する前提条件です。

種類は 2 つあり、この違いは重要です。

分割領域は、1 つの領域を複数の小さな利用可能領域に分けます。折り目は分割領域です。コンテンツがその領域を通って流れることはなく、片側かもう片側に配置されます。

遮蔽領域は、領域を分割せずにその一部を覆います。ビューの境界内にあるフレームとして考えてください。内側の FaceTime カメラは遮蔽領域です。

具体的には、iPhone Duo には予約領域が3 つあります。それぞれの動作が異なるため、どれに当たるかを把握しておくことが大切です。

領域種類表示されるタイミング
外側の前面カメラ遮蔽常時。 角にあり、側面のコントロールと揃っています。また、Live Activities のために Dynamic Island へと広がります。
内側の前面カメラ遮蔽カメラがアクティブな間のみ。ディスプレイの下にあり、通常は見えません。アクティブになると、UI が移動してカメラが見えるようになります。
折りたたみ領域分割デバイスを途中まで折りたたんでいる間のみ。内側のディスプレイを分割し、カーブした中央部分を除外します。

領域はアクティブまたは非アクティブの状態になります。

  • 折り目の分割領域がアクティブになるのは、デバイスを途中まで折りたたんでいる間のみです。完全に開くと非アクティブになります。Tech Talk では、その場合の幅はゼロになると説明されています。一方、後のリファレンスと Group Lab Day 3 では、非アクティブな領域にもフレームが維持されると説明されており、デバイスを折り曲げたときにレイアウトが変わらないよう、あらかじめそのフレームを読み取って折り目を避けることが提案されています。そのサイズを前提にする前に、シミュレーターでフレームを確認してください。
  • カメラの遮蔽領域がアクティブになるのは、そのカメラがストリーミング中の間のみです。

2 つのカメラには非対称性があることに注意してください。外側のカメラはレイアウトの恒常的な前提条件ですが、内側のカメラはその場で現れたり消えたりします。後者は一度だけ読み取る定数ではなく、アニメーションを伴う変化として処理してください。

デフォルトで取得できるのはアクティブな領域のみです。Tech Talks と API リファレンスの説明は一致しています。includeInactive オプションを渡すと、現在は使われていないものの存在する領域も取得できます。レイアウト全体に関わる安定した判断には、こちらが適しています。たとえばグリッドでは、折り目があるすべてのデバイスで偶数の列数を優先することで、ユーザーがデバイスを折り曲げるたびにレイアウトが組み替わるのを防げます。

予約領域のクエリ

SwiftUI では、GeometryReader または onGeometryChange から GeometryProxy を取得し、 予約領域を問い合わせます。2 つのシグネチャは異なります。SwiftUI には UIKit にはない 3 つ目のパラメーターがあります。

// SwiftUI, on GeometryProxy
func reservedRegions(
    kind: ReservedRegion.Kind,
    options: ReservedRegion.QueryOptions,
    layoutDirectionBehavior: LayoutDirectionBehavior
) -> [ReservedRegion]

// UIKit, on UIView
func reservedRegions(
    kind: UIView.ReservedRegion.Kind,
    options: UIView.ReservedRegion.QueryOptions = []
) -> [UIView.ReservedRegion]

layoutDirectionBehavior 引数は、右から左に記述するレイアウトでは重要です。折り目は ハードウェア上の位置に固定されているため、領域の位置をコンテンツと一緒に反転させるか、 物理座標のままにするかは、自分で決める必要があります。自動的に引き継がれるものでは ありません。

options に .includeInactive を渡すと、存在していても現在はアクティブでない領域も 取得できます。

各領域には、矩形以外の情報も含まれています。

  • frame — ビューの座標空間における位置です。
  • isActive(Objective-C では active)— 現在有効かどうかを示します。
  • kind — 分割領域か遮蔽領域かを示します。
  • margins — インタラクティブなコンテンツのためにシステムが確保する余白です。 仕様には、frame にはすでにこれらの余白が含まれると明記されています。そのため、 コントロールを frame の外に置くだけで十分です。margins 分の余白を二重に追加 しないでください。frame を margins 分だけインセットすると、ハードウェアそのもの の領域を取得できます。これはフルブリードのメディア向けの境界であり、メディアは コントロールよりも折り目に近づけられます。
  • id — クエリをまたいで安定しているため、前回確認した領域と新しい領域を見分けられ ます。ReservedRegion は Identifiable なので、領域をそのまま ForEach に渡せます。

フレームはクエリを実行したビューの座標空間で表され、原点 (0, 0) は左上にあります。 また、そのビューと重なる領域だけが返されます。角にかかる領域は、原点が負の値になったり、 ビューの範囲を超えたりすることがあるため、領域がビューの内側に収まるとは限りません。 位置を決めるビューでクエリを実行し、フレームとコンテンツの座標を揃えてください。 注意が必要なケースは 2 つあります。

  • カスタム SwiftUI Layout では、placeSubviews に渡される bounds の原点が、常に (0, 0) とは限りません。位置は原点を基準に計算し、各サブビューを配置するときにのみ bounds.origin を加えてください。
  • UIKit では、updateConstraints() で領域を読み取ってください。UIKit はそこで読み取った 内容を追跡し、領域が変わると再度呼び出すため、独自のオブザーバーは不要です。

SwiftUI のクエリには 3 つ目のパラメーター layoutDirectionBehavior(デフォルトは .mirrors)があり、右から左に記述するレイアウトでは領域のフレームを反転させるため、 カスタム Layout で対応する必要はありません。物理的なフレームを使うには .fixed を 渡してください。

予約領域をクエリするのは、カスタムで手動配置するコントロールの場合です。システム コンテナーを使っている場合は、すでに対応されています。詳しくは以下をご覧ください。

位置調整:デザインパターン

ほとんどのインターフェイスは、予約領域を避けてそのままレイアウトできます。より慎重な 対応が必要なものもあります。そのためのパターンが位置調整です。デバイスを折りたたんだ ときに重要なコンテンツが見え、操作でき、遮られないように、利用可能なスペースに応じて 要素のフレームを調整します。

典型的なケースでは、デバイスを開いている間は要素が中央に配置され、折りたたむと、その 要素の目的に適した領域へ移動します。

適切な範囲を選んでください。 位置調整の対象は、1 つのボタンからコンテナー全体まで さまざまです。単独で機能する要素なら、その要素だけを移動します。複数の要素が一体として 機能するなら、まとめて移動します。コンテキストメニューを、それが属する写真から離すと、 どちらも技術的には見えていても、両者の関係が崩れてしまいます。

要素を大きく移動しないでください。 距離が離れるほど、要素とその元の位置との視覚的な つながりは弱まります。片方を遠くの領域に追いやるより、両方を折り目の近くに揃えるほうが 通常は効果的です。

連続してスクロールするコンテンツの位置調整は避けてください。 記事、フィード、 ドキュメント、リストなどは、スクロールによってすでに適応します。領域間で移動させると 連続性が損なわれ、何の利点もありません。また、スクロール可能なコンテンツは通常、折り目を 避ける必要もありません。曲面の下を通っても問題ありません。

コンテンツを移動する場所

目的に応じて決めます。また、目的はデバイスの姿勢によって変わることに注意してください。

  • 本のように折りたたんだとき — アラートなどの一時的な要素は、デバイスが閉じて外側のディスプレイに移行する際にも表示され続ける側に配置します。
  • テーブルに立てかけたとき — 上部領域には離れた位置から読むコンテンツを配置します。下部領域は安定した面なので、タップ可能なコントロールはそこに配置します。
  • 複数の領域を使える場合 — 状況に合った領域を選びます。フォーカス中の検索フィールドは、検索対象のビューの上に配置し、デバイスを折りたたむと幅と位置を変えます。iPhone でキーボードの上に表示され続けるのと同じです。
移動後にコンテンツをどう適応させるか

位置とサイズを変えるのが一般的ですが、それだけではありません。分割ビューは、左右の列を均等な 50/50 の分割に調整して、両方を表示したままにできます。グリッドは外側の余白を維持し、ヒンジ部分の間隔だけを広げることで、すべてのセルを分割せずに各領域内に収められます。

一貫しているのは、既存のものを移動、サイズ変更、再配置するということです。削除することはありません。デバイスの持ち方にかかわらず、すべてのコンテンツ、すべての機能、すべてのレイアウトにアクセスできます。

自由に適応するコンテナー

これらを自分で実装する前に、システムコンテナーがすでに対応しているか確認してください。

  • ナビゲーション — NavigationStack、NavigationSplitView、TabView
  • コンテンツ — List、ScrollView — ただし、Apple のリファレンスにある注意点として、これらは行を折り目に沿って分割しないため、行が折り目をまたぐことがあります。継続的にスクロールするコンテンツなら問題ありません(上記の移動を参照)。行内のコントロールが曲線部分にかかる場合は、分割を検知し、その行を独自にレイアウトしてください。
  • プレゼンテーション — アクションシート、アラート、メニュー、ポップオーバーは、予約領域を避けて自動的に位置を調整します

アレンジメント

ナビゲーションコンテナーとコンテンツコンテナーの間には、3 つ目の種類があります。ルールに従ってビューを 2 つだけ配置するレイアウトコンテナーです。これは iOS 27.1 で新たに加わったアレンジメントです。

ルールは関数です。入力は、水平および垂直のサイズクラス、ビューの幅と高さの比率、分割領域が現在アクティブかどうかです。出力は、各ビューを表示するかどうかと、ビューに割り当てるフレームです。

アレンジメントを作成する

SwiftUI では、NavigationStack の中に ArrangementView を配置し、プライマリビューとセカンダリビューを指定します。UIKit では、UIArrangementViewController を UINavigationController のルートとして使用し、ビューコントローラーを .primary と .secondary の配置先に割り当てます。

スプリットスタイル

デフォルトのスタイルです。2つのビューの間で表示領域を分割し、特に指定しない限り均等に割り当てます。幅が高さを上回る場合は、プライマリを先にして横並びにし、高さが幅を上回る場合は、プライマリを上にして縦に並べます。

一方の軸だけを使う場合は、スプリットスタイルに .axes(.horizontal) を適用して制約します。UIKit では、同じ方法で UISplitArrangement を設定し、updateArrangement(_:animated:) に渡します。このメソッドと setViewController(_:for:animated:) は、どちらも animated のデフォルト値が false なので、変更をアニメーションさせる場合は true を渡してください。

軸に沿って分割できない場合、つまり軸を除外した場合や、閉じた iPhone Duo を横向きで持っている場合は、適切でないレイアウトになるのではなく、プライマリビューのみを表示します。これを念頭に置いてプライマリを選んでください。折りたたまれずに残るビューです。

均等に半分ずつ分けるのが適切でない場合は、ペインのサイズを調整します。SwiftUI では、ビューに次のいずれかを指定します。コンテナに対する割合を指定する splitArrangementLayoutRatio(_:)、ポイント単位で指定する splitArrangementLayoutSize(minWidth:idealWidth:maxWidth:…)、またはビューの理想サイズを優先する splitArrangementFixedLayoutSize() です。layoutPriority が高いビューが要求したサイズを得て、もう一方が残りを使います。UIKit では、配置に DimensionRange を指定します。

var split = UISplitArrangement.split
var primary = split.defaultViewProperties
var width = UISplitArrangement.DimensionRange()
width.minimum = .absolute(250)
width.preferred = .fractional(0.3)
primary.width = width
split.setViewProperties(primary, for: .primary)
arrangementController.updateArrangement(split, animated: true)

このデバイスでの利点は、セカンダリビューを閉じても、iPad のようにプライマリビューがディスプレイ全体の中央に移動しないことです。折り目で定義された領域内にとどまるため、コントロールに手が届きやすく、曲面にもかかりません。

オーバーレイスタイル

スプリットでは横並びを優先する一方、オーバーレイでは一方のビューをもう一方の上に重ねます。デバイスが折りたたまれると横並びに移動します。これは、重ねたビューが窮屈になってしまうタイミングにちょうど合っています。

具体的には、分割領域が有効でない場合、つまり閉じているか完全に開いている状態では、プライマリビューがセカンダリビューの上に表示されます。部分的に折りたたむと、プライマリは折り目に対して末尾側または下側の領域に移動し、セカンダリは先頭側または上側に移動します。どのビューをプライマリにするかを決める前に、この順序をよく理解しておきましょう。親指が届く位置にくるのがプライマリであり、より広い領域を得るビューではありません。

コンテンツに対してデフォルトの側が適切でない場合は、設定を変更します。SwiftUI ではビューに overlayArrangementEdge(_:) を指定し、UIKit ではオーバーレイのビューのプロパティに edge を指定して、そのビューが横向きのときにどちらの水平エッジを占めるかを選びます。

ポーズによってビュー同士の位置関係が変わるため、従属ビューの表示方法もそれに合わせて変更することがよくあります。重なり状態を読み取って調整してください。SwiftUI では overlayArrangementZIndex 環境値を使い、UIKit では配置のビューコントローラーで state(for:) を呼び出してから、その zIndex を使います。重ねて表示しているときに折りたたまれるリストも、専用の領域が割り当てられると展開できます。

どちらを選ぶか

アプリですでに使われているパターンに従います。HStack/VStack で構成されたものはスプリットに、ZStack で構成されたものはオーバーレイに対応します。どちらもそのまま置き換えられ、iPhone Duo のサポートも実現できます。

参考にできる既存のパターンがない場合は、2つのビューの関係性を考えてください。

  • 前景と背景で、部分的に隠れてもよい場合 — オーバーレイ。読みやすいコンテンツがスクロールする上にコントロールを重ねて表示するのが、典型的な例です。
  • メインと詳細で、どちらも隠れてはならない場合 — スプリット。プレーヤーとそのトランスクリプトが、典型的な例です。
配置を使わない場合
  • 配置にはナビゲーションの仕組みがありません。NavigationSplitView や TabView を配置の中に入れないでください。UIKit では、スプリットビューやタブバーコントローラーも同様です。
  • List、ScrollView、グリッド、コレクションビュー、スプリットビューなど、独自にビューをレイアウトするものの中に配置をネストしないでください。
  • 列を展開・折りたたみする動作が必要な場合は、配置ではなく実際のスプリットビューを使ってください。

作業の進め方

  1. 中央寄せのレイアウトを点検します。2 列レイアウトのほうが適切でしょうか。そうでない場合は、どの変位パターンが該当するでしょうか。
  2. システムのコンテナーとプレゼンテーションを優先します。適応の大部分は自動的に処理されます。
  3. アプリで独自の水平分割やオーバーレイを使用している場合は、ArrangementView に置き換えます。このデバイスだけでなく、サポート対象のすべてのデバイスで iPhone Duo の動作を利用できます。
  4. そのうえで、優先度の高い手動配置のコントロールに限り、予約領域 API を採用して、変位を手動で実装します。

レイアウトではなくヒンジに応じたインタラクションについては、../iphone-duo-hinge-and-scenes/SKILL.md を参照してください。ヒンジのリアルタイム角度はビューのレイアウトには適していませんが、エフェクトの制御には適しています。

コメント