Highlight
このセッションでは SwiftUI におけるウィンドウの定義、オープン、位置、サイズ制御に焦点を当て、クロスプラットフォームゲームアプリ BOT-anist をデモとして使用します。BOT-anist は visionOS 上でエディターウィンドウとゲーム Volume の 2 つの主要シーンを持ち、セッションでは映画再生ウィンドウとコントロールパネルウィンドウを追加し、完全なウィンドウ管理機能を示します。
主要内容
visionOS と macOS では、アプリが 1 つの WindowGroup ですべてを処理している場合、コントロールパネル、詳細ページ、メディア再生などの補助 UI を TabView や NavigationStack に詰め込むしかありません。問題は、ユーザーがこれらの UI を自由にドラッグ、リサイズ、並べて配置できないことです。1 つのウィンドウに詰め込みすぎると、スペースの利用効率が非常に低くなります。
SwiftUI のマルチウィンドウ機能がこの課題を解決します。機能ごとに独立した WindowGroup を定義し、openWindow で新しいウィンドウを開き、pushWindow で現在のウィンドウを置き換え、defaultWindowPlacement で初期位置を制御し、defaultSize と windowResizability でサイズを管理できます。これらの API により各機能モジュールが独自のウィンドウを持ち、ユーザーは独立して移動、リサイズ、閉じることができます。単一ウィンドウ内のレイヤー重ねよりはるかに優れた体験です。
このセッションでは BOT-anist を例に、完全なウィンドウ管理フローをデモします。BOT-anist はもともとエディターウィンドウとゲーム Volume の 2 シーンのみでしたが、セッションでは映画再生ウィンドウとコントロールパネルウィンドウを順次追加し、定義からオープン、配置、サイズ制約までの全プロセスを示します。
詳細
ウィンドウの定義とオープン
BOT-anist の元の Scene 定義には 2 つの WindowGroup があります。エディターとゲームです。ゲームは .windowStyle(.volumetric) で visionOS 上で 3D Volume として表示されます(02:36)。
@main
struct BOTanistApp: App {
var body: some Scene {
WindowGroup(id: "editor") {
EditorContentView()
}
WindowGroup(id: "game") {
GameContentView()
}
.windowStyle(.volumetric)
}
}
- 各
WindowGroupはidで識別され、後で id を参照 .windowStyle(.volumetric)で visionOS 上で 3D Volume 形式で表示
映画ウィンドウの追加は新しい WindowGroup を追加するだけ(03:09):
WindowGroup(id: "movie") {
MovieContentView()
}
エディタービューでは環境変数から openWindow アクションを取得して映画ウィンドウを開きます(03:55):
struct EditorContentView: View {
@Environment(\.openWindow) private var openWindow
var body: some View {
Button("Open Movie", systemImage: "tv") {
openWindow(id: "movie")
}
}
}
@Environment(\.openWindow)で環境からウィンドウを開くアクションを取得openWindow(id: "movie")で WindowGroup の id を渡して開くウィンドウを指定
pushWindow: 重ねるのではなく置き換え
映画ウィンドウとエディターは同時に表示する必要がないため、pushWindow が適しています(04:45):
struct EditorContentView: View {
@Environment(\.pushWindow) private var pushWindow
var body: some View {
Button("Open Movie", systemImage: "tv") {
pushWindow(id: "movie")
}
}
}
pushWindowは新しいウィンドウを開きながらソースウィンドウを非表示にする- 新しいウィンドウを閉じるとソースウィンドウが自動的に復元。追加ロジック不要
- ソースウィンドウと同時にコンテンツを見る必要がない場面に適している
ウィンドウの初期位置制御
新しいウィンドウはデフォルトでソースウィンドウの前方(visionOS)または画面中央(macOS)に表示されます。defaultWindowPlacement でカスタマイズできます(07:46):
WindowGroup(id: "controller") {
ControllerContentView()
}
.defaultWindowPlacement { content, context in
#if os(visionOS)
return WindowPlacement(.utilityPanel)
#elseif os(macOS)
let displayBounds = context.defaultDisplay.visibleRect
let size = content.sizeThatFits(.unspecified)
let position = CGPoint(
x: displayBounds.midX - (size.width / 2),
y: displayBounds.maxY - size.height - 20
)
return WindowPlacement(position, size: size)
#endif
}
WindowPlacement(.utilityPanel)で visionOS 上でユーザー近くの手の届く位置に配置- macOS では
context.defaultDisplay.visibleRectでセーフエリアを取得し、位置を手動計算 content.sizeThatFits(.unspecified)でコンテンツの希望サイズをクエリ- クロージャパラメータ
contentはウィンドウコンテンツのプロキシ、contextはプラットフォーム固有の表示情報
ウィンドウサイズ戦略
defaultSize でウィンドウの初期サイズを設定(10:12):
WindowGroup(id: "movie") {
MovieContentView()
}
.defaultSize(width: 1166, height: 680)
defaultWindowPlacementがサイズを返す場合、またはシーンが復元状態から復元される場合、defaultSizeは無視される- push されたウィンドウのデフォルトサイズはソースウィンドウのサイズと等しい
windowResizability(.contentSize) でコンテンツの min/max に基づくリサイズ範囲を制限(10:49):
WindowGroup(id: "movie") {
MovieContentView()
.frame(
minWidth: 680, maxWidth: 2720,
minHeight: 680, maxHeight: 1020
)
}
.windowResizability(.contentSize)
.frameの minWidth/maxWidth/minHeight/maxHeight でコンテンツのサイズ範囲を定義.windowResizability(.contentSize)でウィンドウサイズがコンテンツの min/max 制約に従う- 映画ウィンドウは最小 680×680 の正方形まで縮小、最大 2720×1020 まで拡大可能
コントロールパネルウィンドウも .contentSize を使用しますが、コンテンツビューは固定サイズ(min/max なし)のため、手動リサイズ不可。モード切り替え時に自動的にサイズ調整されます(11:37)。
プラットフォーム固有の拡張
visionOS では .persistentSystemOverlays(.hidden) でウィンドウバーと閉じるボタンを非表示にし、ユーザーがコンテンツに集中できます(05:48):
WindowGroup(id: "movie") {
...
}
.persistentSystemOverlays(.hidden)
Freeform スタイルの toolbar ornament や ToolbarTitleMenu も visionOS でよく使われるウィンドウ拡張手段です。
重要ポイント
- やること: 補助機能を独立したウィンドウに分割する。コントロールパネル、設定パネル、詳細プレビュー、メディアプレーヤーはすべてマルチウィンドウの候補。なぜやる価値があるか: visionOS と macOS では独立ウィンドウによりユーザーが自由に配置・リサイズでき、単一ウィンドウ内のレイヤー重ねよりスペース利用効率がはるかに高い。始め方: 補助機能用に
id付きの新しいWindowGroupを作成し、メインビューからopenWindow(id:)で開く。 - やること: コンテンツを同時に見る必要があるかに応じて
pushWindowかopenWindowを選択。なぜやる価値があるか:pushWindowはソースウィンドウの非表示と復元を自動管理し、手動の閉じ/再開ロジックやウィンドウの積み重ねによる視覚的混乱を回避。始め方:@Environment(\.openWindow)を@Environment(\.pushWindow)に置き換え。呼び出し方法は同じ。 - やること:
defaultWindowPlacementで新しいウィンドウの合理的な初期位置を設定。なぜやる価値があるか: デフォルト位置は既存コンテンツを遮るかユーザーから遠すぎる可能性がある。良い初期位置は手動調整の負担を減らす。始め方: WindowGroup に.defaultWindowPlacementmodifier を追加。visionOS では.utilityPanel、macOS ではcontext.defaultDisplay.visibleRectで位置を計算。 - やること:
windowResizability(.contentSize)+ コンテンツの min/max frame でウィンドウのリサイズ範囲を制御。なぜやる価値があるか: ウィンドウがコンテンツが使えなくなるまで縮小されたり、過剰なスペースを占有するまで拡大されるのを防ぐ。始め方: コンテンツビューの.frameで minWidth/maxWidth/minHeight/maxHeight を設定し、WindowGroup に.windowResizability(.contentSize)を追加。
関連セッション
- Bring your iOS or iPadOS game to visionOS — iOS/iPadOS ゲームを visionOS に移行する完全ガイド
- Create custom environments for your immersive apps in visionOS — イマーシブアプリ用カスタム環境の作成
- Create enhanced spatial computing experiences with ARKit — ARKit でイマーシブな空間コンピューティング体験を構築
- Design interactive experiences for visionOS — visionOS 上のインタラクティブなナラティブ体験の設計
コメント
GitHub Issues · utterances