SwiftUI Multiplatform Design Guide について
Apple各プラットフォーム向けのSwiftUIアプリ設計ガイドです。単なるコード共有ではなく、デバイスごとにシーン、状態、操作方法を適応させる設計を扱います。
主な機能
- 01シーンとウィンドウの設計
- 02ウィンドウごとの状態管理
- 03ドキュメント中心のワークフロー
- 04ImmersiveSpaceの遷移
- 05クロスプラットフォームの入力とレイアウト
活用シーン
- 01iOSとmacOSアプリの設計
- 02複数ウィンドウ間の状態分離
- 03ドキュメント型アプリの設計
- 04visionOSの没入空間遷移の処理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mosif16/ codex-skills --skill swiftui-multiplatform-design-guideインストール後、Claude Code で/swiftui-multiplatform-design-guideを入力して読み込まれたか確認します。
詳細
SwiftUI における統合アーキテクチャ:iOS、iPadOS、macOS、visionOS 向けの包括的な設計戦略
1. はじめに:空間コンピューティングと平面コンピューティングの融合
現代の Apple ソフトウェア開発を取り巻く環境は、操作パラダイムが急速に融合していることを特徴としています。かつて、プラットフォーム間の境界は明確でした。iOS はタッチターゲットと画面への直接操作が中心であり、macOS は精密なカーソルとマウスによる間接的な操作が中心でした。一方、iPadOS は両者をつなごうとする中間的な位置を占めていました。しかし、visionOS の登場によって、この階層的なアーキテクチャは根本的に変化しました。画面サイズへの対応だけでなく、物理的な次元、入力方法、ユーザーの意図にも適応する新しいデザイン言語が必要になったのです。
このレポートでは、SwiftUI を使って真のマルチプラットフォームアプリケーションを構築するために必要なアーキテクチャ戦略を徹底的に分析します。「クロスプラットフォーム」と呼ばれることの多い、表面的なコード共有――しばしば「最大公約数」にとどまるアプローチ――を超えて、各デバイス固有の使いやすさを尊重する、状態駆動型の統合アーキテクチャを提唱します。Apple のビジュアル言語の基盤となっている「Liquid Glass」というメタファー、シーンとウィンドウの複雑な階層、そしてタップ、クリック、視線によるピンチ操作のいずれからも同じ意味的アクションを実行できる統合入力モデルについて解説します。
1.1 ヒューマンインターフェイスガイドラインの進化
Apple ヒューマンインターフェイスガイドライン(HIG)は、プラットフォームごとの静的なルール集から、流動的で包括的なデザイン哲学へと進化してきました。この哲学の中核となる柱――空間、没入感、パススルー、空間オーディオ――は、もはやヘッドセットだけのものではありません。^1^ これらは「空間コンピューティング」への移行を示すものであり、平面インターフェイスにも影響を及ぼします。たとえば、visionOS のアイコンに見られるレイヤーや奥行きの表現は、tvOS の視差効果や iOS の滑らかなアニメーションと概念的につながっています。^2^
この進化から得られる重要な洞察の一つは、「パススルー」デザインという考え方です。これは、現実世界が見えるという光学的な意味だけでなく、インターフェイスによる妨げを最小限に抑えるという意味でもあります。iOS では、半透明のマテリアルやシートとして表現され、visionOS では文字どおりの透明性として表現されます。このデザインには、「コンテンツ優先」のアプローチが必要です。つまり、「クローム」(インターフェイス要素)を後景に退かせます。HIG では、UI を中心とする体験にはウィンドウを使い、没入感のある表示は特別な場面に限るよう明示的に推奨しています。このパターンは iOS のモーダル表示にも通じます。^1^
1.2 マルチプラットフォームにおけるアプリアイコンの役割
アプリケーションへの入り口であるアプリアイコンは、マルチプラットフォームへの適応を最初に学ぶ機会となります。単なるアセットとして見過ごされがちですが、アイコンの振る舞いはユーザーの期待を左右します。iOS では、アイコンは平坦なスーパー楕円形で、ホーム画面のレイアウト変更に応じて変化します。macOS では、ドロップシャドウによってデスクトップのメタファーを表現する、統一された「チクレット」として機能します。一方、tvOS と visionOS では、アイコンはレイヤーを重ねた立体的なオブジェクトになります。
こうしたアイコンの構造に関する調査からは、レイヤーが融合して立体感を生み出す「Liquid Glass」の美学が見えてきます。visionOS のアイコンは通常、背景レイヤーと 1 つまたは 2 つの前景レイヤーで構成されます。ユーザーがアイコンを注視すると、システムがハイライトと視差効果を適用し、前景レイヤーを物理的に浮かび上がらせてユーザーの目に近づけます。^2^ これは単なる装飾ではありません。「見る」こと自体が操作であるとユーザーに教える役割を果たします。そのため、効果的なマルチプラットフォームアーキテクチャでは、このレイヤー構造の考え方をアプリ内にも広げ、UI 要素を平坦なピクセルではなく、階層の中に浮かび、視線やポインターの「フォーカス」に反応する準備のできた平面として扱う必要があります。
2. シーンの階層:ウィンドウ、ボリューム、スペース
SwiftUI のライフサイクルでは、Scene プロトコルがアプリケーション構造の基本単位として View に取って代わりました。View がユーザーに見えるものを定義するのに対し、Scene はオペレーティングシステムがそのコンテンツの存在、永続性、ライフサイクルをどのように管理するかを定義します。Scene の種類、特に WindowGroup、DocumentGroup、ImmersiveSpace を使いこなすことは、iPhone SE から Vision Pro まで規模を拡大できるアプリを構築するための前提条件です。
2.1 WindowGroup のパラダイムとライフサイクル管理
WindowGroup は SwiftUI の主力です。アプリケーションの主要なインターフェイスのテンプレートを定義します。ただし、その動作はホストプラットフォームによって大きく変わります。iPhone では、通常、単一の永続的なウィンドウをインスタンス化します。iPadOS と macOS では、複数のウィンドウを同時に作成するためのファクトリーとして機能します。^4^
この多態性により、状態管理に関してアーキテクチャが大幅に複雑になります。SwiftUI の初期の開発でよく見られたアンチパターンは、App レベルで注入されるシングルトンの AppState オブジェクトに依存することでした。単一ウィンドウの iPhone では機能しますが、このアーキテクチャは iPadOS と macOS では破綻します。ユーザーがデータを比較するために同じアプリケーションのウィンドウを 2 つ開いた場合、シングルトンのアーキテクチャでは両方のウィンドウが同じナビゲーションパスと選択状態を共有します。ウィンドウ A でのナビゲーションが意図せずウィンドウ B のナビゲーションも動かし、独立した作業領域というユーザーのメンタルモデルを損ないます。
この問題に対処するため、現代のマルチプラットフォームアーキテクチャでは、状態を Scene ごとにスコープする必要があります。WindowGroup が新しいウィンドウをインスタンス化するたびに、そのウィンドウのビュー階層に対して新しい依存関係グラフを実質的に立ち上げる必要があります。このため、多くの場合、アプリのルートではなく、ウィンドウのルートビューの環境に注入する、ウィンドウごとの ViewModel または NavigationManager を作成します。^6^
2.1.1 ウィンドウのサイズ変更とジオメトリ
「ウィンドウ」の概念は、物理環境によって異なります。macOS では、ウィンドウは画面のベゼル内に収まる長方形であり、ユーザーが自由にサイズを変更できます。visionOS では、ウィンドウは 3D 空間に浮かぶ平面状のサーフェスです。こちらもサイズを変更できますが、視認性と視野によって制約されます。
SwiftUI には、これを制御するための .windowResizability および .defaultSize 修飾子が用意されています。
-
コンテンツサイズ戦略: .windowResizability(.contentSize) を使用すると、ウィンドウのサイズを SwiftUI コンテンツに合わせてぴったり調整できます。これは設定パネルやユーティリティダイアログに最適です。^4^
-
自動戦略: サイズはシステムが決定します。visionOS では、ユーザーとの距離がこの動作に影響します。遠くにあるときに角度上「小さく」見えるウィンドウも、近くにある「大きな」ウィンドウと同じピクセル解像度で描画されている場合があります。システムは視認性を維持するため、コンテンツを自動的に拡大縮小します(ダイナミックタイプ)。そのため、開発者は空間環境で文字が極端に小さくならないよう、ピクセルではなく「ポイント」を基準にレイアウトする必要があります。^1^
2.2 DocumentGroupとファイルベースのワークフロー
生産性アプリケーションでは、DocumentGroupのシーンタイプが不可欠です。プラットフォームのファイルシステム(macOSではFinder、iOS/iPadOSではファイル)と、すぐに使える形で統合できます。さらに重要なのは、デスクトップクラスのアプリケーションに必要となる、複雑な「未保存状態」の管理やファイルロックの仕組みを自動化できることです。^8^
DocumentGroupを導入すると、SwiftUIはアプリケーションのドキュメントモデル(FileDocumentに準拠)をシステム標準のメニューコマンドに自動的に連携します。macOSの「ファイル > 開く」、「ファイル > 保存」、「ファイル > 複製」の各メニュー項目は、アクティブなドキュメントシーンに自動的に接続されます。これは、AppKitの定型コードを何百行も書かずに実現できる、SwiftUIの重要な「無償」の機能です。
ただし、標準的なアプリからDocumentGroupアプリに移行するには、データフローを根本的に見直す必要があります。信頼できる情報源はクラウドデータベースやローカルキャッシュではなく、ディスク上のファイルになります。これはvisionOSにも影響します。visionOSでは、他のアプリと共有するスペースでファイルを操作することがあるためです。visionOSのDocumentGroupはファイルをフローティングウィンドウとして表示するため、ユーザーは複数のドキュメント(PDFやスプレッドシートなど)を周囲の空間に配置できます。これにより、物理的な部屋を実質的にマルチモニターのデスクトップに変えられます。^8^
2.3 ImmersiveSpaces:仮想現実における排他性
ImmersiveSpaceのシーンタイプは、統合アーキテクチャの中で最も大きな違いを表します。コンテンツを収めるコンテナであるウィンドウとは異なり、ImmersiveSpaceではアプリが環境そのもののコンテナになります。
ImmersiveSpaceにおけるアーキテクチャ上の重要な制約は、排他性です。visionOSでは複数のアプリが「共有スペース」にウィンドウを開けますが、任意の時点で「イマーシブスペース」を占有できるアプリは1つだけです。App Aがイマーシブ状態にある場合、App Bのイマーシブコンテンツは非表示になります。そのため、遷移ロジックを厳密に管理する必要があります。イマーシブスペースを開く処理(openImmersiveSpace)は非同期操作であり、システムによって失敗したり、キャンセルされたりすることがあります。^10^
2.3.1 「ローディングウィンドウ」遷移パターン
異なるイマーシブコンテキスト間を遷移するとき(たとえば、「火星」環境から「月」環境へ移動するとき)に、特有の課題が生じます。開けるスペースは1つだけなので、アプリは次のスペースを開く前に、現在のスペースを閉じなければなりません。しかし、ウィンドウを開く前に現在のスペースを閉じると、アプリで開いているシーンが実質的にゼロになります。visionOSでは、開いているシーンがゼロのアプリはシステムによって終了されます。
この問題を回避するため、熟練した開発者は「ローディングウィンドウ」パターンを採用します。
-
**トリガー:**ユーザーが新しい環境を選択します。
-
**アクション:**アプリが小さな一時的なウィンドウ(たとえば、「読み込み中...」のスピナー)を開きます。
-
**同期:**ウィンドウが表示されたことを確認してから(scenePhaseまたはonAppear経由で)、アプリは現在のImmersiveSpaceを閉じます。
-
**遷移:**アプリが新しいImmersiveSpaceを要求します。
-
**後処理:**新しいスペースの表示が成功したら、アプリはローディングウィンドウを閉じます。
この「既存を維持してから切り替える」ロジックは、VR環境でコンテキストを頻繁に切り替える際のアプリのクラッシュを防ぐために不可欠です。^12^ 非同期のシーントポロジを扱う際の状態管理がいかに複雑になるかを示しています。これは、iOSの同期的なナビゲーションスタックには存在しない問題です。
2.4 ScenePhase ステートマシン
これらのシーンのライフサイクルは、ScenePhase を使って監視します。この環境値は、シーンが .active、.inactive、または .background のいずれであるかを示します。^13^
-
アクティブ: シーンが入力イベントを受け取っている状態です。
-
非アクティブ: シーンは表示されていますが、フォーカスされていない状態です。macOS では、バックグラウンドのウィンドウにあたります。iPadOS では、Split View の操作できない側にあたります。visionOS では、ユーザーがウィンドウから目をそらしたときや、Shared Space で別のアプリを操作したときによく発生する状態です。
-
バックグラウンド: シーンが表示されていない状態です。
バッテリー駆動時間とパフォーマンスを維持するうえで、特に visionOS では .inactive 状態への対応が重要です。アプリがボリュームウィンドウで負荷の高いパーティクルシミュレーションを実行している場合、.inactive 状態に入った時点(つまり、ユーザーが目をそらしたとき)で、ただちにシミュレーションを一時停止する必要があります。対応しないと、ユーザーが見ていないコンテンツに GPU リソースを大量に浪費します。^14^
表 1:プラットフォームごとのシーンの機能と制約
機能 iOS iPadOS macOS visionOS
並行性 シーンは 複数のウィンドウ 複数の 複数の 1つ (ほとんどの ウィンドウ ウィンドウと (ほとんどの 場合) ボリューム 場合)
ウィンドウの システムのみ Split View / ユーザー ユーザー サイズ変更 Stage Manager (自由形式) (均一な スケール)
座標空間 2D 画面 2D 画面 2D 画面 3D の無限空間 (Shared Space)
イマーシブ 対象外 対象外 対象外 排他的 モード (1つのアプリ のみ)
終了 バックグラウンド スワイプ/ Cmd+Q/ シーンが への移行時 バックグラウンド時 最後のウィンドウ 0個になったときに を閉じたときに 終了
3. アダプティブレイアウト戦略:サイドバーからオーナメントまで
5インチのiPhoneからVision Proの無限のキャンバスまで、画面領域は大きく異なります。そのため、コンテンツを引き伸ばすだけのレスポンシブな戦略ではなく、階層を変化させるアダプティブなレイアウト戦略が求められます。あらゆる用途に NavigationSplitView を使う従来の方法では、複雑な最上位ナビゲーションに対応しきれないことが分かり、新しい sidebarAdaptable の考え方が採用されるようになりました。
3.1 最上位ナビゲーションにおける NavigationSplitView の衰退
長年、NavigationSplitView(およびその前身である NavigationView)は、「マスター・ディテール」インターフェースの標準でした。サイドバー、コンテンツ列、詳細領域を提供します。iPad と macOS では、階層データ(例:メール、メモ)の閲覧に適しています。
しかし、NavigationSplitView は、互いに関連のない独立した機能領域(例:「ライブラリ」「ラジオ」「検索」を備えた音楽アプリ)を持つアプリの ルート ナビゲーションコンテナとしては扱いにくいものです。Split View を使うと、これらの個別の領域が単一のリストにまとめられ、モバイルデバイスでは単純化されすぎた印象になりがちです。さらに visionOS では、NavigationSplitView は特定のガラス素材を使って描画され、サイドバーは Z 空間で少し後方に配置されます。見た目は美しいものの、Split View を入れ子にすると(例:Tab View の中に Split View を配置する)、視覚的に煩雑になることがあります。また、macOS の一部のバージョンでは、ツールバーの競合が原因でアプリがクラッシュすることもあります。^15^
3.2 SidebarAdaptableの革新
iOS 18とmacOS 15で、AppleはTabView用の .tabViewStyle(.sidebarAdaptable)修飾子を導入しました。これはレイアウトアーキテクチャにおける根本的な変化であり、タブバーとサイドバーの概念を、流動的に変化する単一のコンポーネントへと実質的に統合します。^17^
この修飾子を使うと、標準のTabViewは水平サイズクラスに応じて表示モードを適応させられます。
-
コンパクト幅(iPhone/iPadのSlide Over): 従来の下部タブバーとして表示されます。
-
レギュラー幅(iPadのフルスクリーン): 画面上部に配置されるタブバーとして表示され、ユーザーはサイドバーへの切り替えができます。
-
macOS: ネイティブの半透明マテリアルを採用したサイドバーとして自動的に表示されます。
-
visionOS: メインウィンドウの左側に浮かぶ縦型の「オーナメント」に変化します。^17^
これにより、「サイドバーとタブバー」のコード分岐の問題が解決します。開発者は、もはやif os(iOS) { TabView } else { NavigationSplitView }のように書く必要はありません。単一のTabViewを記述し、システムが操作性に適したコンテナを表示することに任せられます。
3.2.1 階層的な情報密度を実現するTabSection
sidebarAdaptableと組み合わせる重要な要素がTabSectionです。下部タブバーではスペースが限られているため、通常は3~5項目しか表示できません。一方、サイドバーでは縦方向にスクロールできるため、数十項目を表示できます。TabSectionを使うと、開発者はタブを論理的にグループ化できます。^19^
-
iOSでの動作: 下部バーが煩雑にならないよう、TabSectionのヘッダーは通常、統合されるか表示されません。
-
macOS/iPad(サイドバー)での動作: TabSectionのヘッダーは、独立した折りたたみ可能な見出し(「ライブラリ」「プレイリスト」「設定」など)として表示されます。
これにより、「情報密度に適応する」デザインが可能になります。階層をTabSectionで構成するだけで、サイドバーモードではフォルダーなどの詳細なナビゲーション項目を表示し、タブバーモードではそれらを単一の「ブラウズ」タブにまとめられます。^20^
3.3 深い階層のナビゲーション状態を保持する
適応型レイアウトにおける最も難しい課題の一つは、レイアウトコンテナが変化したときにナビゲーション状態を保持することです。ユーザーがiPadのサイドバーモードでナビゲーションスタックの深い階層にいて、別のアプリとSplit Viewを作成すると、iPadの幅がコンパクトになり、アプリが下部タブバーへの切り替えを強いられる場合があります。
ナビゲーション状態がビュー階層内(例:ビュー内の@State)に保存されている場合、この切り替えによって状態が失われ、ユーザーはルートビューに戻されます。
アーキテクチャ上の解決策: ナビゲーション状態はViewの外部に持ち上げ、ModelまたはEnvironmentに格納する必要があります。NavigationStack(path: $navigationModel.path)を使うと、ナビゲーションスタックのパスをUIコンテナから切り離せます。TabViewがサイドバーからタブバーへ変化したとき、新しいNavigationStackインスタンスが同じ永続的なパスオブジェクトにバインドされるため、ユーザーは切り替え前に表示していたのとまったく同じ詳細画面にシームレスに戻れます。^21^
3.4 ViewThatFits を使ったマイクロレイアウト
sidebarAdaptable がマクロ構造を処理する一方で、個々のセルやパネルのレイアウトには ViewThatFits が必要です。このコンテナは基本的にレイアウトの論理ゲートとして機能します。ビューのクロージャを受け取り、切り詰められることなく利用可能なスペースに収まる最初のビューを描画します。^23^
これは visionOS では実質的に必須です。固定されたポイント寸法を持つ iOS デバイスとは異なり、visionOS のウィンドウはほぼあらゆるアスペクト比にサイズ変更できます。幅 1000pt で見栄えのよい横向きカードレイアウトも、幅 400pt では崩れて見えることがあります。ViewThatFits を使うと、開発者は同じコンポーネントに「横向き」バージョンと「縦向き」バージョンを用意できます。
Swift
ViewThatFits(in:.horizontal) {
HStack { /* Wide layout */ }
VStack { /* Tall layout */ }
}
この宣言型のアプローチは、レイアウトサイクルやパフォーマンスの低下を引き起こすことが多かった、従来の壊れやすい GeometryReader パターンに取って代わります。^23^ これにより、アプリの内部コンポーネントは外側のコンテナと同じように柔軟になります。
4. 入力モデル:3 つのモードによるインタラクションシステム
プラットフォーム間の最も根本的な違いは、ユーザーがどのように意図を表現するかにあります。「3 モード」システムには、直接操作(タッチ)、間接ポインティング(マウス/トラックパッド)、意図に基づくターゲティング(視線/ピンチ)が含まれます。統一されたアーキテクチャでは、これらの物理的な違いを意味的なアクションに抽象化する必要があります。
4.1 タップと空間インタラクションの統合
タッチインターフェースの初期には、TapGesture は単純なものでした。2D デジタイザー上の座標です。visionOS では事情が複雑になります。visionOS での「タップ」は実際には複雑な一連の動作です。ユーザーの視線でエンティティを狙い(ホバー)、指をピンチして確定します。このタップの「位置」は画面上ではなく、視線のレイと仮想オブジェクトのコライダーが交差する位置です。^25^
SwiftUI では SpatialTapGesture によってこれを統一しています。
-
iOS/macOS の場合: ビューを基準としたクリック/タップの 2D 位置を報告します。
-
visionOS の場合: エンティティ表面上のレイキャスト交点の 3D 位置を報告します。
これはインタラクティブな 3D コンテンツに不可欠です。たとえば、ボリューム型の太陽系アプリでは、SpatialTapGesture を使うと、ユーザーは 3D の月モデル上の特定のクレーターをタップできます。この処理を行うコードはプラットフォーム間で共有でき、iOS 版では 3D ビューの平面投影上のタッチ座標として解釈するだけです。^26^
4.2 視線とホバーエフェクトの仕組み
visionOS では、「カーソル」はユーザーの目です。しかし、マウスカーソルとは異なり、視線は揺れやすく、無意識に動きます。誤操作(「ミダス・タッチ」の問題)を防ぐため、選択が行われるまで、visionOS は視線の座標をアプリケーションに通知しません。
その代わりに、システムはフィードバックを提供するためにホバーエフェクトを使用します。視線がインタラクティブな要素に留まると、システムがその要素をハイライトします。このフィードバックは、要素を見つけやすくするうえで不可欠です。visionOS では、ホバーエフェクトのないボタンは「反応しない」ように感じられ、ユーザーは静的なテキストだと思い込んでしまいます。
アーキテクチャ上の要件: マルチプラットフォームアプリのすべてのカスタムインタラクティブビューに、.hoverEffect() 修飾子を適用する必要があります。
-
iPadOS: ポインターへの磁力効果と形状の変化を追加します。
-
visionOS: 半透明の光彩、または「浮き上がり」効果を追加します。
-
macOS: 通常は無視されるか、標準のカーソル変更に割り当てられます。
Apple の新しい CustomHoverEffect プロトコル(iOS 18 以降)では、細かな制御が可能です。開発者は、フォーカス時にビューをどのように拡大・縮小するか、暗くするか、影を付けるかを正確に定義できます。これにより、ポインターを操作する Mac と、視線で操作するヘッドセットの間で、「フォーカス」のビジュアル表現が統一されます。^28^
プライバシーへの影響: アプリはユーザーの視線の先を認識できないため、ツールチップのような「ホバーで表示」に依存する仕組みは、システムが処理するか、明確なジェスチャ(視線を長く留めるなど)を必要とします。ユーザーが明示的に操作しない限り、開発者は視線を留めた時間だけをもとに、プログラムからロジックを実行することはできません。^30^
4.3 間接入力:フォーカスエンジン
iOS ではタッチが主な入力方法ですが、macOS ではキーボード操作が主であり、visionOS でも(生産性を重視する場合)視線入力と同等に重要です。FocusState API がその橋渡しをします。
FocusState を適切に実装すると、Tab キーでアプリを操作できるようになります。これは macOS だけの機能ではありません。Vision Pro に Bluetooth キーボードを接続すれば、同じように Tab キーで移動できます。onTapGesture のみに依存するアプリは、キーボードユーザーが利用できなくなります。Button と FocusState を使うと、「フォーカスリング」(macOS)または「フォーカスハイライト」(visionOS)が UI 内を論理的な順序で移動するようになります。^31^
4.4 ジェスチャの組み合わせと競合
マルチプラットフォームアプリでは、ジェスチャの競合が頻繁に発生します。iOS の List では、縦方向のスクロールを扱います。visionOS の List では、スクロールに「ピンチしてドラッグ」を使います。開発者がリストの行にカスタム DragGesture(「スワイプして削除」やカスタムスライダーなど)を設定すると、visionOS でのスクロールジェスチャを妨げる可能性があります。
ベストプラクティス:
-
標準コンポーネントを使う: 標準の List スワイプアクションは、すべてのプラットフォームで自動的に機能します。
-
対象を絞ったジェスチャ: visionOS で RealityView を使う場合は、 .gesture(MyGesture().targetedToEntity(myEntity)) を使います。これにより、ジェスチャの適用対象が特定の 3D オブジェクトに限定され、ウィンドウレベルのスワイプが妨げられるのを防ぎます。^27^
-
同時認識: .simultaneousGesture は控えめに使いますが、スクロールビュー内のボタンで、スクロールビューがドラッグの開始を認識する前にタップを認識させる必要がある場合には、多くの場合これが必要です。^33^
5. コマンドと制御の基盤
ツールバーを中心とする iOS と、メニューを中心とする macOS の違いは、大きな摩擦の原因です。真にネイティブな macOS アプリでは、コマンドを見つけやすくするためにメニューバーが使われます。一方、iOS では目に見えるボタンが使われます。
5.1 再考されたレスポンダーチェーン:FocusedValue
SwiftUI では、FocusedValue と Commands を通じて「レスポンダーチェーン」の概念が再導入されています。これにより、トリガー(メニュー項目)とアクション(ビューのロジック)を明確に分離できます。
「削除」コマンドを例に考えてみましょう。
-
トリガー: メニューバーの「編集」メニューにある「削除」項目。
-
アクション: 選択した項目を削除する、特定のビュー内の関数。
マルチウィンドウアプリでは、メニューバーはグローバルです。どのウィンドウのコンテンツを削除すればよいか、どのように判断するのでしょうか?
その解決策が FocusedValue です。
-
ビューで、FocusedValueKey に準拠する構造体を定義します。
-
ビューに .focusedValue(\.deleteAction, deleteFunction) を適用します。
-
コマンドで @FocusedValue(\.deleteAction) var deleteAction を読み取ります。
ユーザーがメニューで「削除」をクリックすると、システムはフォーカスされている要素からビュー階層をたどり、最も近くで定義された deleteAction を探します。この「最も内側の定義を優先する」ロジックは、AppKit のレスポンダーチェーンを完全に再現しながら、型安全で宣言的です。^34^
5.2 コンテキストメニューとメニュー
contextMenu 修飾子は、次のように対応します。
-
iOS: 触覚タッチ(長押し)。
-
macOS: 右クリック。
-
visionOS: 長いピンチ(または専用ボタン)。
visionOS に関する警告: Apple のガイドラインでは、visionOS でコンテキストメニューを多用しないよう推奨しています。「長いピンチ」ジェスチャは、右クリックやタップよりも身体的な負担が大きいためです。筋肉の緊張を保ち続ける必要があります。そのため visionOS では、iOS ではコンテキストメニューの中にあるアクションを、Ornament や専用の「その他」(...)ボタンなどを使って、トップレベルの UI に表示することが望ましい場合があります。^19^
新しい .contextMenu(forSelectionType:) 修飾子は、リストの各行に .contextMenu を適用する方法よりも推奨されます。この新しい修飾子はリストの選択項目を対象とするため、パフォーマンスに優れ、macOS や iPadOS の複数選択機能にもより適しています。^36^
5.3 Ornaments:空間ツールバー
visionOS では、ツールバーの代わりに Ornaments を使用します。SwiftUI は .toolbar のコンテンツを自動的に下部の Ornament に対応付けますが、開発者は .ornament(attachmentAnchor:) 修飾子を使って「側面の Ornament」を明示的に作成できます。
アーキテクチャ上、Ornaments はウィンドウに「固定」されていますが、別の Z 平面に存在します。ツールパレット(ブラシや色など)の配置場所として理想的です。常に表示しておく必要がありながら、邪魔にならないからです。固定的な macOS のツールバーとは異なり、Ornaments はユーザーがつかんで動かせると直感的に理解できる「浮遊パレット」の感覚を実現します(ただし、現状ではウィンドウと一緒に移動します)。^37^
6. visionOS 固有の仕様:共有空間の座標
visionOS 向けの開発では、2D 座標から 3D 変換へと考え方を切り替える必要があります。「共有空間」(アプリがウィンドウとして表示される空間)であっても、座標系は重要です。
6.1 ボリュームと Z 深度
.windowStyle(.volumetric) を適用した WindowGroup は、ボリュームを作成します。これは境界のある 3D ボックスです。
-
クリッピング: ボリューム内のコンテンツは、その境界でクリップされます。3D キャラクターをボリュームの外へ歩かせることはできません。
-
インタラクション: ユーザーはボリューム全体を回転できます。そのため、アプリではデバイスの向きだけでなく、ユーザーに対するコンテナの向きの変化にも対応する必要があります。
ボリュームでよくある問題は、スケールです。汎用的な USDZ モデルを読み込むと、実世界の単位で高さが 100 メートルになっていることがあります。ボリューム内では、これは極端に小さく表示されるか、完全にクリップされてしまいます。開発者は、すべての 3D アセットのスケールを標準的なボリュームの寸法(通常は 1~2 メートル)に合わせて正規化する必要があります。^7^
6.2 アプリの「正面」
iOSでは、「正面」は画面を指します。visionOSでは、「正面」はアプリの起動時点におけるユーザーの頭の向きを基準とします。
システムには、すべてのアプリを再センタリングするリセット機能(Digital Crownを長押し)が用意されています。アプリケーションはこの操作を検知し、コンテンツの位置を再調整する必要があります。
ただし、開発者は、ユーザーの空間内の特定の座標へウィンドウをプログラムから移動させることはできません(共有スペースの場合)。これは、アプリが壁にスパムを重ねたり、ユーザーの動きを追跡したりするのを防ぐためのプライバシー上の制約です。ウィンドウの変換を所有するのはユーザーであり、アプリが所有するのはウィンドウ内のコンテンツだけです。1
7. アクセシビリティとインクルージョンをアーキテクチャの基盤に
マルチプラットフォーム設計では、アクセシビリティはコンプライアンス要件として扱われがちです。しかし、SwiftUIとvisionOSの文脈では、アーキテクチャの中核をなす柱です。
7.1 ターゲットサイズと物理特性
-
iOS: ターゲットサイズの最小値は44x44ポイントです。
-
visionOS: ターゲットサイズの最小値は60x60ポイント(仮想)です。
なぜ違いがあるのでしょうか。視線追跡には自然な「揺れ」(サッカード)があります。60ptより小さいターゲットは、安定して注視するのが困難です。密度の高いiOSツールバーを間隔を調整せずにvisionOSへ移植すると、ユーザーは頻繁に誤タップすることになります。
解決策:visionOSのレイアウトでは、アダプティブなパディングまたはViewThatFitsを使って間隔を広げます。38
7.2 アクセシビリティアクション
.accessibilityAction修飾子を使うと、複雑なジェスチャをシンプルな意味的アクションに割り当てることができます。
-
シナリオ: 「振って取り消す」ジェスチャや、iPadでの「3本指スワイプ」。
-
問題: 運動制御に制限のあるユーザーは、これらの操作を実行できません。visionOSのユーザーにとっても、負担になる可能性があります。
-
解決策: .accessibilityAction(named: "Undo") { undo() }。
これにより、アクションがVoice Control、Switch Control、アクセシビリティメニューで利用可能になり、入力方法にかかわらずアプリの機能を使えるようになります。visionOSでは、一部のユーザーが「ピンチ」ジェスチャを正確に行えず、Dwell Controlや音声に頼る場合があるため、これは非常に重要です。^38^
8. 実装戦略:コード構造
この統合アーキテクチャを実現するには、コードベースを特定の構造パターンに従って構成する必要があります。
8.1 「Core-UI」の分離
-
コアロジック(モデル): Swiftのみで構成します。可能な限り、SwiftUIをインポートしません。データ、永続化、ビジネスロジックを処理します。
-
状態レイヤー(ViewModel): ObservableObjectまたは@Observableを使用します。データをUIの状態に対応付けます(例:「サイドバーは開いているか?」「現在の選択項目は何か?」)。
-
インターフェースレイヤー(ビュー): 高度にモジュール化されたSwiftUIビューを使用します。
-
アトム: ボタン、ラベル(.hoverEffect、.contentShapeでスタイル設定)。
-
モレキュール: リスト行、カード(レイアウトにViewThatFitsを使用)。
-
オーガニズム: NavigationSplitView / TabView(sidebarAdaptableを使用)。
-
8.2 マルチプラットフォームのテスト
テストはシミュレーターだけでは不十分です。
-
タッチのシミュレーション: iPad用のMacシミュレーターはレイアウトの確認には正確ですが、「太い指」の問題はシミュレートできません。
-
視線のシミュレーション: visionOSシミュレーターでは、マウスを使って視線をシミュレートできます。機能テストには便利ですが、ユーザビリティテストにはまったく適していません。実際のヘッドセットなしでは、眼精疲労やターゲットを捉える難しさを評価できません。
-
ポインターのシミュレーション: iPadでは、トラックパッドを接続して「ホバー」効果をテストする必要があります。Macでは問題なく見えるホバー状態でも、磁気効果によりiPadでは「引っかかる」感じがしたり、違和感があったりすることがよくあります。^39^
9. 結論
iOS、iPadOS、macOS、visionOS の融合は、ハードウェアの融合ではありません。デバイスは物理的には別々のままですが、アーキテクチャの融合です。visionOS が導入した「空間的」なデザイン言語はフラットなプラットフォームにも浸透し、エコシステム全体に奥行き、レイヤー構造、意図を認識するインタラクティブ性をもたらしました。
一貫性のあるマルチプラットフォームアプリを構築するには、開発者は「デバイス中心」の考え方(例:「iPhone なら X を行う」)を捨て、「コンテキスト中心」の考え方(例:「幅がコンパクトなら X を行う。ポインティングデバイスが接続されていれば Y を行う」)を取り入れる必要があります。Scene の階層構造、sidebarAdaptable レイアウトエンジン、FocusedValue コマンドチェーン、統一された SpatialTapGesture 入力モデルを活用することで、開発者は手元、デスク、部屋のいずれにも自然に馴染むアプリケーションを構築できます。Apple 開発の未来とは、あらゆる場所で実行できるコードを書くことだけではなく、あらゆる場所にふさわしいコードを書くことです。
表 2:統一入力マッピング戦略
**セマンティック **iOS iPadOS macOS visionOS アクション (タッチ) (タッチ/ポインター) (ポインター/キーボード) (視線/手)
主な選択 タップ タップ/クリック クリック 見つめてピンチ
二次アクション 長押し 長押し/右クリック 右クリック 長いピンチ/ (触覚) 「その他」ボタン
スクロール スワイプ スワイプ/2 本指 スクロールホイール/ ピンチ+手を パン トラックパッド ドラッグ
ズーム ピンチで拡大 ピンチで拡大 ピンチ(トラックパッド)両手で拡大
フォーカス/対象 N/A(直接操作) ホバー(磁気的) ホバー(カーソル) 視線(ホバー効果)
テキスト入力 仮想 仮想/物理キー 物理キーボード 仮想/ キーボード ボード 音声入力
表 3:ライフサイクル状態の影響
ScenePhase システムの動作 開発者の責任 (一般) **
Active アプリに CPU/GPU の アニメーションを開始し、 優先度が最大限与えられます。 ソケットを接続し、 センサーを有効にします。
Inactive アプリは表示されていますが、 負荷の高いレンダリング フォーカスされていません (ゲームの場合)を一時停止し、 (例:Mac のバックグラウンド 下書き状態を保存します。 ウィンドウ)。
Background アプリは表示されていません。 重要: 負荷の高い まもなく一時停止される場合が リソースを解放し、 あります。 ファイルハンドルを閉じ、 位置情報の更新を停止します。 visionOS では、すべての ARKit セッションを一時停止します。
表 4:コマンドインフラストラクチャの実装
コンポーネント 役割 実装
メニューバー グローバルなコマンド .commands { CommandMenu(...) 検出 } on WindowGroup.
FocusedValue 「レスポンダーチェーン」 @FocusedValue(\.key) var の連携 action in Commands; .focusedValue(\.key, value) in Views.
KeyboardShortcut パワーユーザー向けの .keyboardShortcut("s", 起動 modifiers:.command) on Buttons.
AccessibilityAction 支援技術による起動 .accessibilityAction(named: "Save") { save() }.
参考文献
-
visionOS 向けの設計 | Apple Developer ドキュメント、閲覧日 2025 年 11 月 24 日、 [https://developer.apple.com/design/human-interface-guidelines/designing-for-visionos]{.underline}
-
アプリアイコン | Apple Developer ドキュメント、閲覧日 2025 年 11 月 24 日、 [https://developer.apple.com/design/human-interface-guidelines/app-icons]{.underline}
-
ヒューマンインターフェイスガイドライン | Apple Developer ドキュメント、 閲覧日 2025 年 11 月 24 日、 [https://developer.apple.com/design/human-interface-guidelines]{.underline}
-
ウィンドウとスペースの表示 | Apple Developer ドキュメント、 閲覧日 2025 年 11 月 24 日、 [https://developer.apple.com/documentation/visionos/presenting-windows-and-spaces]{.underline}
-
複数のウィンドウをサポートする — アプリ開発チュートリアル | Apple Developer ドキュメント、閲覧日 2025 年 11 月 24 日、 [https://developer.apple.com/tutorials/app-dev-training/supporting-multiple-windows]{.underline}
-
シーン。SwiftUI がどうなっているのか疑問に思ったことはありますか……| Michael-Andre Odusami 著 - Medium、閲覧日 2025 年 11 月 24 日、 [https://medium.com/@michaelodusami/swiftui-scenes-29778723d48b]{.underline}
-
イマーシブスペースにウィンドウを追加する方法。- visionosdev - Reddit、 閲覧日 2025 年 11 月 24 日、 [https://www.reddit.com/r/visionosdev/comments/1ctn03e/how_to_add_windows_in_an_immersive_space/]{.underline}
-
SwiftUI でメニューバーを構築し、カスタマイズする | Apple Developer ドキュメント、閲覧日 2025 年 11 月 24 日、 [https://developer.apple.com/documentation/SwiftUI/Building-and-customizing-the-menu-bar-with-SwiftUI]{.underline}
-
タブナビゲーションでアプリのコンテンツを強化する | Apple Developer ドキュメント、閲覧日 2025 年 11 月 24 日、 [https://developer.apple.com/documentation/SwiftUI/Enhancing-your-app-content-with-tab-navigation]{.underline}
-
ImmersiveSpace | Apple Developer Documentation、2025年11月24日アクセス、 [https://developer.apple.com/documentation/SwiftUI/ImmersiveSpace]{.underline}
-
visionOS のイマーシブスペースを探る - Create with Swift、2025年11月24日アクセス、 [https://www.createwithswift.com/exploring-immersive-spaces-in-visionos/]{.underline}
-
あるイマーシブスペースから別のイマーシブスペースに遷移する方法 — パート 2 - Step Into Vision、2025年11月24日アクセス、 [https://stepinto.vision/example-code/how-to-transition-from-one-immersive-space-to-another-part-two/]{.underline}
-
ScenePhase | Apple Developer Documentation、2025年11月24日アクセス、 [https://developer.apple.com/documentation/swiftui/scenephase]{.underline}
-
シーンフェーズとのインタラクティブ性:SwiftUI での @Environment(.scenePhase) の活用 | Wesley Matlock 著 | Medium、2025年11月24日アクセス、 [https://medium.com/@wesleymatlock/interactivity-with-scene-phases-harnessing-environment-scenephase-in-swiftui-935da9c36874]{.underline}
-
Navigation Split View を探る - Create with Swift、2025年11月24日アクセス、 [https://www.createwithswift.com/exploring-the-navigationsplitview/]{.underline}
-
Axel Le Pennec:「新しい TabView デザインについて…」- iOS Dev Space、2025年11月24日アクセス、 [https://iosdev.space/@alpennec/113465183815948633]{.underline}
-
sidebarAdaptable | Apple Developer Documentation、2025年11月24日アクセス、 [https://developer.apple.com/documentation/SwiftUI/TabViewStyle/sidebarAdaptable]{.underline}
-
iOS 18 の新しい TabView でサイドバーを使う - Donny Wals、2025年11月24日アクセス、 [https://www.donnywals.com/using-ios-18s-new-tabview-with-a-sidebar/]{.underline}
-
iOS 18 で改善された SwiftUI の TabView とサイドバーを使う - iOS Coffee Break、2025年11月24日アクセス、 [https://www.ioscoffeebreak.com/issue/issue34]{.underline}
-
sidebarAdaptable TabView スタイルでトップバーからサイドバーへ - YouTube、2025年11月24日アクセス、 [https://www.youtube.com/watch?v=YcrMLBmz0dA]{.underline}
-
ウィンドウ/画面の幅に応じて NavigationSplitView と TabView の両方を採用する、2025年11月24日アクセス、 [https://stackoverflow.com/questions/79665096/adopting-both-navigationsplitview-and-tabview-depending-on-window-screen-width]{.underline}
-
SwiftUI Navigation Stack で理想的な TabView の動作を実現する | Akshay Mahajan 著、2025年11月24日アクセス、 [https://betterprogramming.pub/swiftui-navigation-stack-and-ideal-tab-view-behaviour-e514cc41a029]{.underline}
-
SwiftUI iOS 16 の ViewThatFits をマスターする | Husnain Ali 著 | Medium、2025年11月24日アクセス、 [https://medium.com/@husnainali593/mastering-viewthatfits-in-swiftui-ios-16-9af7e642a84a]{.underline}
-
SwiftUI レイアウトの不思議な事例:Spacer が思ったとおりに動作しない理由 | Wesley Matlock | Medium、2025 年 11 月 24 日にアクセス、 [https://medium.com/@wesleymatlock/the-curious-case-of-swiftui-layout-why-that-spacer-isnt-doing-what-you-think-86d7cede277c]{.underline}
-
visionOS での操作を掘り下げる、2025 年 11 月 24 日にアクセス、 [https://stepinto.vision/articles/deep-dive-into-manipulation-on-visionos/]{.underline}
-
SpatialTapGesture | Apple Developer Documentation、2025 年 11 月 24 日にアクセス、 [https://developer.apple.com/documentation/SwiftUI/SpatialTapGesture]{.underline}
-
visionOS のイマーシブスペースでジェスチャーを検出する(SwiftUI) - Stack Overflow、2025 年 11 月 24 日にアクセス、 [https://stackoverflow.com/questions/77712163/detect-gesture-in-immersive-space-visionos-swiftui]{.underline}
-
Spatial SwiftUI:hoverEffect モディファイア - Step Into Vision、2025 年 11 月 24 日にアクセス、 [https://stepinto.vision/example-code/spatial-swiftui-hovereffect-modifier/]{.underline}
-
CustomHoverEffect | Apple Developer Documentation、2025 年 11 月 24 日にアクセス、 [https://developer.apple.com/documentation/SwiftUI/CustomHoverEffect]{.underline}
-
Eyes | Apple Developer Documentation、2025 年 11 月 24 日にアクセス、 [https://developer.apple.com/design/human-interface-guidelines/eyes]{.underline}
-
SwiftUI の FocusState プロパティラッパーを使いこなす - Swift with Majid、2025 年 11 月 24 日にアクセス、 [https://swiftwithmajid.com/2021/08/24/mastering-focusstate-property-wrapper-in-swiftui/]{.underline}
-
アタッチメントビューと空間タップジェスチャーの両方の押下を検出する方法 - Reddit、2025 年 11 月 24 日にアクセス、 [https://www.reddit.com/r/visionosdev/comments/1ezumz7/how_to_detect_pushing_both_of_attachment_view_and/]{.underline}
-
SwiftUI でマルチタッチジェスチャーを認識する - Stack Overflow、2025 年 11 月 24 日にアクセス、 [https://stackoverflow.com/questions/74475743/recognizing-a-multitouch-gesture-with-swiftui]{.underline}
-
SwiftUI の FocusedValues - Shadowfacts、2025 年 11 月 24 日にアクセス、 [https://shadowfacts.net/2025/focused-values/]{.underline}
-
visionOS で高度なホバーエフェクトを作成する - Create with Swift、2025 年 11 月 24 日にアクセス、 [https://www.createwithswift.com/creating-advanced-hover-effects-in-visionos/]{.underline}
-
SwiftUI の Menu で見つけた小さなモディファイア。メニューを長押しせずにタップしたとき、メニューをボタンとして動作させることができます。長押しするとメニューアクションが表示されます。どうやら iOS 15 からあるようです - Reddit、2025 年 11 月 24 日にアクセス、 [https://www.reddit.com/r/SwiftUI/comments/1i5hxlm/small_modifier_i_found_with_the_swiftui_menu_when/]{.underline}
-
visionOS における WindowGroup と ImmersiveSpace Entity の接続、2025年11月24日閲覧、 [https://stackoverflow.com/questions/78169409/connection-between-windowgroup-and-immersivespace-entity-on-visionos]{.underline}
-
SwiftUI を使用した iOS モバイルアプリの一般的なアクセシビリティの課題への対処方法、2025年11月24日閲覧、 [https://www.freecodecamp.org/news/how-to-address-ios-accessibility-challenges-using-swiftui/]{.underline}
-
クロスプラットフォームのタッチ操作を実現する:ポインターイベントとタッチイベントの比較 - Christian Liebel、2025年11月24日閲覧、 [https://christianliebel.com/2015/05/enabling-cross-platform-touch-interactions-pointer-vs-touch-events/]{.underline}