visionOSの空間パターン——ウィンドウの先にあるもの
visionOS向けに公開されているアプリの多くは、Appleの「Designed for iPad」互換パスを通じてこのプラットフォームに届いています。既存のiPadバイナリが3D空間に浮かぶ平面パネルとして動作し、開発者はvisionOSネイティブの体験を作り込むのではなく、チェックボックスを一つ入れるだけで済むわけです。ユーザーにとっては何も問題はありません(アプリは動きます)。しかし、それではプラットフォームの価値を安売りしていることになります。visionOSのネイティブな表現面は、3種類の提示方法(Window、Volume、Immersive Space)に加えて、iPadのSDKには存在しない構造的なUIプリミティブ(Ornament、Attachment)を開発者に与えてくれます。4 これらを取り入れたアプリはネイティブに感じられ、取り入れないアプリは「Vision上のiPadアプリ」として読まれてしまいます。
この記事では、Appleのドキュメントに沿って空間表現の語彙をたどっていきます。visionOS入門ではなく、「このプラットフォームがSwiftUIアプリに実際に何を提供しているのか」という視点で組み立てました。同シリーズのRealityKitと空間のメンタルモデルが3Dコンテンツの層を扱うのに対し、本稿はそれを収めるSwiftUI側の表現面を扱います。
TL;DR
- visionOSアプリは3種類のシーンを組み合わせて構成します。
WindowGroup(Window)、.windowStyle(.volumetric)を付けたWindowGroup(Volume)、そしてImmersiveSpace(Immersive Space)です1。 - Windowは2Dの平面、Volumeは境界を持つ3D領域、Immersive Spaceはユーザーを取り囲む空間です。それぞれ規則が異なります。Volumeは生成後のサイズが変えられず、Immersive Spaceは明示的な開閉が必要で、Windowが最もiPadに近い振る舞いをします。
- 没入には3つのスタイルがあります。
.mixed(コンテンツが現実の部屋と共存)、.full(部屋が仮想環境に置き換わる)、.progressive(周辺視野で現実とのつながりを保つ中間段階)2。 - OrnamentはWindowと平行に、z軸方向へ少し手前に配置されるUI平面です。visionOSにおけるツールバーやタブバーはこれで実現します3。AttachmentはRealityView内の3DコンテンツにSwiftUIビューを埋め込む仕組みで、平面のUIと空間の幾何をつなぐ橋渡しになります。
- 「パネルアプリ」というアンチパターン——iPadのUIをそのままWindowとして出し、Volume、Space、Ornamentのどれも採用しない形です。ユーザーは使えますが、プラットフォーム本来の価値は手つかずのまま残ります。
3つのシーン種別
visionOSアプリのApp本体は、3つのクラスからシーンを組み立てます。それぞれユーザー側のメンタルモデルがはっきり異なります。
Window——2Dの平面
WindowGroupは、既定でvisionOSのガラスフレームを備えた2DのWindowを生成します。Windowは空間内に配置され(システムがユーザーの視線の先に置きます)、標準のシステムジェスチャによって移動やリサイズが行われます。SwiftUIの観点から見ると、WindowはmacOSのウィンドウに相当する存在です。つまり、奥行きを意識したガラス素材をまとった平面のコンテンツ面ということになります。
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
既定のWindowは、コンテンツの周囲にガラス素材を持ちます。完全に透明な面が欲しい場合は.windowStyle(.plain)を使います。
WindowGroup {
ContentView()
}
.windowStyle(.plain)
plainスタイルのWindowはシステムのガラスフレームを失います。コンテンツ側が独自の視覚的な器を用意している場合に使ってください。それ以外では既定のままが正解です。
Volume——境界を持つ3D領域
Volumeは、奥行きを持つコンテンツ(モデル、複数オブジェクトからなるシーン、第三の軸があると活きるUI)を収める3D領域です。VolumeのシーンもまたBaseはWindowGroupであり、スタイルだけが異なります。
WindowGroup(id: "globe") {
GlobeView()
}
.windowStyle(.volumetric)
.defaultSize(width: 0.6, height: 0.6, depth: 0.6, in: .meters)
.defaultSize(width:height:depth:in:)修飾子は、Volumeの境界を実世界の単位(メートル)で指定します。既定では境界は開いた時点で固定され、ユーザーは移動はできてもリサイズはできません。visionOS 2以降では、.windowResizability(.contentSize)と関連するAPIによって、ユーザーがリサイズできるVolumeをオプトインで実現する道も加わりました。とはいえ、固定サイズという既定が依然として最も一般的なケースです。つまり、既定サイズは慎重に選ぶ必要があります。開発者が明示的にオプトインしない限り、多くのVolumeはリサイズできないままだからです。
Volumeが向いているのは、空間的な境界そのものが体験の一部になっているアプリです。ユーザーが周りを歩ける仮想彫刻、現実の壁に留めたメジャー、奥行き方向にずらして配置したターゲットを持つワークアウトシーン——といったものが該当します。単により広いキャンバスが欲しいだけのアプリはVolumeから何も得られません。その場合は、より大きなWindowが正しい答えです。
Immersive Space——取り囲むもの
ImmersiveSpaceは、ユーザーの周囲の環境を占有するシーンです。5 WindowやVolume(どちらもShared Space内で他のアプリと並んで見えます)とは違い、Immersive Spaceはユーザーの周囲を引き受け、他のアプリのウィンドウを同時に使うことを妨げます。
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
ImmersiveSpace(id: "training") {
TrainingScene()
}
.immersionStyle(selection: .constant(.mixed), in: .mixed, .progressive, .full)
}
}
.immersionStyle(...)修飾子は、体験の段階を選ぶためのものです。
.mixed仮想コンテンツが現実の部屋と並んで現れます。ユーザーが両方の文脈から恩恵を受けるアプリ向けです。.progressiveDigital Crownで度合いを上下できる部分的な没入です。中央の視界は仮想でありながら、周辺視野では部屋の存在を保ち続けられます。.full部屋が仮想環境に置き換わります。完全な没入体験(瞑想、トレーニングシミュレーション、ゲーム)で使われます。
Immersive Spaceを開く操作は明示的です。アプリは@Environment(\.openImmersiveSpace)をSpaceのidとともに呼び出し、遷移アニメーションと競合するSpaceの終了はシステムが処理します。
@Environment(\.openImmersiveSpace) var openImmersiveSpace
@Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace
Button("Start Session") {
Task {
await openImmersiveSpace(id: "training")
}
}
1つのアプリでアクティブにできるImmersive Spaceは、同時に1つだけです。Space間の遷移(たとえば.mixedから.fullへ)には、古いSpaceを明示的に閉じてから新しいSpaceを開く手順が必要となります。
Ornament——Windowを取り巻くUI平面
Ornamentは、Windowの縁に取り付けられ、Window平面よりz軸方向にわずかに手前へ配置されるSwiftUIビューです。visionOSでは、これがツールバーやタブバー、補助的なコントロールの実現手段になります。システム自身も随所でOrnamentを使っています。TVアプリの再生コントロール、Musicのセグメンテッドコントロール、Mailのツールバーがそうです。
ContentView()
.ornament(
attachmentAnchor: .scene(.bottom),
contentAlignment: .center
) {
HStack {
Button("Previous", systemImage: "backward.fill") { ... }
Button("Play", systemImage: "play.fill") { ... }
Button("Next", systemImage: "forward.fill") { ... }
}
.padding()
.glassBackgroundEffect()
}
attachmentAnchor:パラメータは、OrnamentがWindowに対してどこに座るかを指定します。.scene(.top)、.scene(.bottom)、.scene(.leading)、.scene(.trailing)が使えます。Ornamentの見た目の処理は開発者の責任です。.glassBackgroundEffect()を使えば、Windowのフレームと調和するvisionOSネイティブのガラス素材が得られます。
Ornamentは、visionOS特有の実際的な問題を解きます。コントロールをWindowの中に置けばコンテンツが窮屈になり、別のWindowに置けばユーザーは視線を移し直さなければなりません。Ornamentはユーザーの周辺視野に浮かび、視線で狙える一方で、中央の視界をめぐって主コンテンツと競合しないのです。
RealityViewのAttachment——3D空間の中のSwiftUI
3DシーンのなかにSwiftUIビューが必要になったとき(3Dモデルに付くラベル、仮想オブジェクトの近くに浮かぶボタン、現実の面に留めた計測値の表示など)、その橋渡しになるのがRealityViewのAttachment機構です。
RealityView { content, attachments in
let model = ModelEntity(...)
content.add(model)
if let label = attachments.entity(for: "label") {
label.position = [0, 0.5, 0]
model.addChild(label)
}
} attachments: {
Attachment(id: "label") {
Text("Vintage Globe, 1872")
.padding()
.glassBackgroundEffect()
}
}
attachments:クロージャは、安定した識別子を持つSwiftUIビューを宣言します。メインのRealityViewクロージャの内側では、attachments.entity(for:)がそのビューを3DのEntityとして取得し、シーンの座標空間内に配置できるようにします。このビューは3Dシーンではテクスチャ付き平面としてレンダリングされながら、SwiftUIの更新サイクルにも参加します(状態が変われば再描画されます)。
ワールド内UIであれば、この仕組みがそのまま正解です。動くオブジェクトを追いかけるラベル、計測の注釈、文脈に応じたボタン——いずれも同じです。SwiftUIビューの書き方は変わらず、3D上の配置はRealityViewの層で完結します。
「パネルアプリ」というアンチパターン
visionOSで最もよくある失敗は、パネルアプリです。「Designed for iPad」互換でvisionOSにやってきたiPadアプリが、Volumeも、Immersive Spaceも、Ornamentも持たない単一のWindowとして公開される形です。アプリは動きます。ただ、プラットフォームに見合ったものにはなっていません。
パネルアプリであることを示す兆候が3つあります。
単一のWindowシーンしかない。 .windowStyle(.volumetric)もなく、ImmersiveSpaceの宣言もない。アプリは平面であり、それ以上のものではありません。
Ornamentを採用していない。 タブバーがWindowの外ではなく、コンテンツの内側に住んでいます。結果として、同じ情報密度のvisionOSネイティブなアプリよりも窮屈になります。
空間ならではの機能がない。 第三の軸を何にも使っていない状態です。Volume内の3Dモデルもなければ、Space内の環境シーンもなく、Attachmentによるz方向のUI配置もありません。iPadでやっていたことをそのままやっている——ただ宙に浮いているだけ、というわけです。
パネルアプリ自体は失敗ではありません。空間コンピューティングから恩恵を受けないカテゴリ(チャットアプリ、メモアプリ、設定ユーティリティ)にとっては、それが正しい選択です。失敗となるのは、パネルアプリを出しておきながらそれをvisionOSネイティブの本格対応だと称するときです。同シリーズのAppleプラットフォーム・マトリクスでは、プラットフォームへの参入は製品判断であると論じました。visionOSにおけるその判断は、「このアプリは空間という表現面を得るに値するのか、それともパネルで十分なのか」という問いになります。
よくある失敗
visionOSのUXを損なうパターンを3つ挙げます。
実質2Dのコンテンツに奥行きの余白を足しただけのVolume。 Volumeを満たしていても、その内側で平面ばかりを描画する「3D」UIは、貴重な空間を無駄にします。Volumeは3Dコンテンツのためのもの。平面のコンテンツはWindowに属します。
用途と噛み合わない没入スタイル。 .full没入しか用意しない瞑想アプリは、短時間の利用でもユーザーを自分の環境から引き剥がしてしまいます。逆に.mixedしか用意しないトレーニングアプリは、完全に集中したい運動には踏み込みが足りません。没入スタイルは、ユーザーが実際に過ごす時間の性質に合わせましょう。
コンテンツと張り合うOrnament。 Ornamentは設計上、周辺的な存在です。中央の注意を要求するOrnament(点滅する色、動くアニメーション)は、その目的を台無しにします。Ornamentは、安定していてちらりと見れば分かるコントロールに使ってください。
このパターンがvisionOSアプリにとって意味すること
持ち帰りたい点が3つあります。
-
シーン種別は、実装のしやすさではなくユーザーのメンタルモデルで選ぶ。 平坦なリストはWindow。ユーザーが眺め回す3DモデルはVolume。取り囲む環境はImmersive Space。これらを1つのアプリで組み合わせること(必要に応じてVolumeを開くWindow、Windowのボタンから入れるImmersive Space)が、visionOSネイティブなパターンです。
-
ツールバーや補助UIにはOrnamentを採用する。 「このUIは補助的なものだ」とvisionOSが伝える手段がOrnamentです。ツールバーをWindowのコンテンツ内に置けば「Vision上のiPadアプリ」として読まれます。組み込みの手間は小さく、見え方の差は大きいのです。
-
RealityView内のワールド内UIにはAttachmentを使う。 3Dオブジェクトに付くラベル、仮想コンテンツの近くのボタン、文脈に応じた数値表示。SwiftUIと3D空間の橋渡しはすでに解決済みです。失敗するのは、これを使わずに場当たり的な3Dテキスト描画に行き着くときです。
Apple Ecosystemシリーズの全体像はこちらです。型付きのApp Intents、MCPサーバー、ルーティングの問い、Foundation Models、実行時と開発ツールのLLMの違い、3つの表現面、単一の真実の源というパターン、2つのMCPサーバー、Apple開発のためのフック、Live Activities、watchOSの実行時契約、SwiftUIの中身、RealityKitの空間メンタルモデル、SwiftDataのスキーマ規律、Liquid Glassのパターン、マルチプラットフォームでの公開、プラットフォーム・マトリクス、Visionフレームワーク、Symbol Effectsの語彙、Core MLのオンデバイス推論、Writing Tools API、Swift Testing、Privacy Manifestの深掘り、プラットフォーム機能としてのアクセシビリティ、SF Proのタイポグラフィ体系、私が書かないと決めていること。ハブはApple Ecosystemシリーズにあります。AIエージェントを絡めたiOS開発のより広い文脈は、iOSエージェント開発ガイドをご覧ください。
FAQ
VolumeとImmersive Spaceは何が違うのですか?
Volumeは境界を持つ3D領域で、他のアプリと並んでShared Spaceに存在します。ユーザーはその周りを歩けますし、システムが枠を与え、他のアプリのWindowも見えたままです。一方Immersive Spaceはユーザーを取り囲み、環境を引き受け、他のアプリの同時利用を妨げます。Volumeは「この3Dのものを見る」ため、Spaceは「この環境の中にいる」ためのものと考えてください。
複数のVolumeを同時に開けますか?
開けます。.volumetricを指定したWindowGroupのシーンは、それぞれ独自のサイズとコンテンツを持って同時に開けます。システムがそれらを空間内で独立に配置します。
複数のImmersive Spaceを同時に開けますか?
開けません。1つのアプリでアクティブにできるImmersive Spaceは同時に1つだけです。Space間を切り替えるには、@Environment(\.openImmersiveSpace)と@Environment(\.dismissImmersiveSpace)を使い、現在のSpaceを明示的に閉じてから新しいSpaceを開く必要があります。
Volumeのサイズは本当に変更できないのですか?
Volumeの境界は、既定では開いた時点で固定されます。visionOS HIGの考え方としては、Volumeは意図した境界を持つ特定の3Dコンテンツを表すものであり、ユーザーが自由にリサイズするとコンテンツ本来のスケールが歪んでしまう、という整理です。visionOS 2以降では.windowResizability(.contentSize)と関連するAPIによって、リサイズ可能なVolumeを開発者がオプトインで選べるようになりました。ユーザーがサイズを変えられる空間的な器が必要なアプリは、それを要求できます。とはいえ、多くのVolumeは固定という既定のまま公開されており、特定のスケールを持つコンテンツ(仮想彫刻、実寸のモデルなど)についてはHIGも引き続きこれを推奨しています。
visionOSのWindowにタブバーを付けるにはどうすればよいですか?
コンテンツ内のタブ(iPad流のパターン)ならWindow内でTabViewを使い、visionOSネイティブな周辺配置のタブUIなら、ボタン列を自作したOrnamentを使います。Apple自身のアプリ(Music、Mail)が採っているのはOrnamentの道であり、visionOSのユーザーにとって最もネイティブに感じられるのもこちらです。
RealityViewのAttachmentはハンドトラッキングと連携できますか?
できます。Attachmentは配置された時点で3Dのエンティティとなり、他のRealityKitエンティティと同じジェスチャおよびヒットテストの仕組みに参加します。タップ、ドラッグ、ホバーの各ジェスチャはSwiftUI標準のジェスチャ修飾子で付けられます。ハンドトラッキングとの統合パターンについては、同シリーズのRealityKitの記事で扱っています。
参考文献
-
Apple Developer: Meet SwiftUI for spatial computing(WWDC 2023 セッション10109)。visionOSの3つのシーン種別として、WindowGroup、volumetricなWindowGroup、ImmersiveSpaceを紹介したセッション。 ↩
-
Apple Developer Documentation:
ImmersionStyle。3つの没入スタイル(.mixed、.progressive、.full)と.immersionStyle(selection:in:)修飾子のAPI。 ↩ -
Apple Developer Documentation:
ornament(visibility:attachmentAnchor:contentAlignment:ornament:)。指定したアンカーでWindowにOrnamentのUI平面を追加するSwiftUIのビュー修飾子。 ↩ -
Apple Developer: Go beyond the window with SwiftUI(WWDC 2023 セッション10111)。Volume、Immersive Space、そしてvisionOSで平面パネルUIの先へ進むためのパターンを扱ったセッション。 ↩
-
Apple Developer Documentation: Creating an immersive space in visionOS with SwiftUI。Immersive Spaceの定義から起動までを一通り解説したガイド。 ↩