ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
SwiftUIでのウインドウの操作
visionOS、macOS、iPadOSで、シングルウインドウとマルチウインドウの優れたアプリを構築する方法を学びましょう。ウインドウを開く/閉じる操作をプログラムで実行したり、位置やサイズを調整したり、ウインドウを別のウインドウと交換したりできるツールをご紹介します。また、ユーザーが各自のワークフロー内でアプリを使用しやすいウインドウを実現するための、デザインの原則についても確認します。
関連する章
- 0:00 - Introduction
- 1:19 - Fundamentals
- 6:15 - Placement
- 9:43 - Sizing
- 12:03 - Next steps
リソース
関連ビデオ
WWDC24
WWDC23
-
ダウンロード
こんにちは SwiftUI担当のAndrewです 本セッションでは SwiftUIアプリの ウインドウについて説明します
ウインドウとは アプリのコンテンツを 格納する入れ物です
ウインドウにより 使い慣れた操作方法で アプリの様々な部分を管理できます
位置の変更や
サイズの変更
閉じるなどの操作です
今回の説明には BOT-anistを使います 私が同僚と開発している SwiftUIアプリです この画面は Simulatorでの BOT-anistのロボットエディタです ロボットをカスタマイズする画面です
プレイヤーは このロボットがゲーム内で 植物の世話をするのを手伝います
BOT-anistはiOS iPadOS visionOS macOSで パーソナライズされた体験を提供します
今回説明する概念はマルチウインドウの プラットフォームにも適用可能ですが 本ビデオでは visionOSのみを対象とします
まず ウインドウの定義と作成 および使用の方法について説明します
次に ウインドウの初期位置を 制御する方法を解説します また ウインドウのサイズ変更の 各種方法をご紹介します
まずは基本事項です
独立した複数の ウインドウを使うと アプリの 様々な部分を同時に扱えます
同じインターフェイスのインスタンスを 複数作成すると効果的な場合もあります
システムコントロールを使えば 各ウインドウを個別に操作できます
サイズの変更や位置の変更 拡大などの操作が可能です
また ウインドウでは プラットフォーム固有の機能を利用できます 例えば visionOSでは ボリュメトリックウインドウスタイルにより ウインドウに3Dコンテンツを表示できます
複数ウインドウの使用にも利点がありますが TabViewのように最上位メニューのみを 表示する単一のビューは ユーザー体験をシンプル化します
「Elevate your windowed app for spatial computing」では TabViewやその他の最上位ビューの 詳細について解説しています visionOSで複数ウインドウの使用が 適している場合の詳細については 「Design for spatial user interfaces」をご覧ください visionOSのBOT-anistには 主要なシーンが2つあります エディタウインドウと ゲームのボリュームです
各シーンはWindowGroupで 定義されています アプリでは ロボットエディタの WindowGroupの インスタンスが開きます このウインドウのボタンで ゲームのWindowGroupの インスタンスが開きます windowStyleを .volumetricにして ウインドウをvisionOSの ボリュームにします BOT-anistに新しい機能を 2つ追加しましょう
1つ目は ロボットに関する動画を表示する 新しいウインドウを開く機能です
この動画はポータルに含まれる 3Dシーンになります
アプリのbodyに 3Dシーンビューを格納する 新しいWindowGroupを追加します このWindowGroupを識別できるように 「movie」というIDを指定しています このIDはウインドウを開く際に使用します
このIDを Environmentアクションに渡します これらのアクションは SwiftUIの 階層構造のどこからでも使用できます ウインドウ管理には 複数の Environmentアクションを使用します
ウインドウを開くには openWindowを使用します
閉じるには dismissWindowを使用します
pushWindowを使うと ウインドウを 開いて元のウインドウを非表示にできます
ここではopenWindowを使って 新しい動画ウインドウを開きます ロボットエディタのビューで Environmentから openWindowアクションを 取得するために openWindowのキーパスを指定して Environmentプロパティを作成します 次に 新しいボタンで openWindowアクションを実行しますが ここで 先ほどWindowGroup用に 定義したIDである「movie」を指定します
これで エディタのボタンをタップすると 動画のポータルが 別ウインドウで表示されるようになりました
こうして見てみると 動画のビューと同時に エディタを 表示しておく必要はないと思えます そこで Environmentアクションの pushWindowを ウインドウ表示に使用します この方法では 新しいウインドウが 元のウインドウと入れ替わりに開きます
新しいウインドウを閉じると 元のウインドウが再度表示されます
動画ウインドウ表示時に エディタを非表示にするには Environmentプロパティの キーパスを openWindowから pushWindowに変更し 前と異なるアクションを呼び出すように ボタンを更新します
これで TVのボタンをタップすると 動画ウインドウがプッシュされ エディタウインドウが消えるようになります
これで デザインしたロボットが 演技を始めるのを 気を散らされることなく 見られるようになりました
閉じるボタンをタップすると エディタに戻ります この動作を実現するために ロジックの追加は必要ありません ウインドウを開く時に 表示しておく 必要のないコンテンツがある場合は このアクションの使用を検討してください
ウインドウを定義して開いたら プラットフォーム固有の機能を使って さらに快適になるように機能を強化できます 例えば フリーボードで ツールバーオーナメントを使って ウインドウの下端に沿って コントロールを表示する方法や ToolbarTitleMenuで 画面を混雑させずに ドキュメントの関連アクションを 表示する方法などです
ウインドウバーと閉じるボタンは デフォルトで必ず表示されます しかし 動画ビューでは .persistentSystem Overlaysモディファイアを使って これらを非表示にしました ユーザーが 動画に集中しやすくなるためです これらのAPIは visionOSのウインドウの 機能を強化する優れた手段です
macOSでの ウインドウの調整については 「Tailor macOS windows with SwiftUI」をご覧ください 動画ウインドウの表示を改善できたので 次に ゲームで使用するオプションの コントロールパネルを追加します このパネルには ロボットを動かすための コントロールと ジャンプや手を振るなどのアクションを 実行するボタンを表示します
コントロールを表示する 新しいWindowGroupを追加しました
また ゲームのボリュームに openWindowの呼び出しを追加します
ゲームでボタンをタップすると 新しい ウインドウでコントロールが表示されます
ゲームのボリュームから独立して 位置を変えられる点が優れています
しかし 初めてウインドウを開くと 表示がボリュームに重なるか 位置が遠い場合もあります visionOSでは コントロールパネルなどの 新しいウインドウは 元のウインドウの前面に配置されます
一方macOSでは 新しいウインドウは 画面の中央に表示されます
この動作は defaultWindowPlacement モディファイアでカスタマイズでき ウインドウの初期位置とサイズを プログラムで設定できます
ウインドウの位置やサイズの変更方法は プラットフォームごとに複数あります
配置には 前面や背面など ほかのウインドウを基準とする相対配置や visionOSのutilityPanel のようにユーザーを基準とする 相対配置があります 後者ではウインドウを ユーザーの近くの 通常は直接タッチできる範囲に配置します
または macOSでの右上エリアのように 画面に対する相対配置もあります
visionOSでゲームコントロールを プレイヤーの近くに表示するには defaultWindowPlacement モディファイアをcontrollerの WindowGroupに適用します ここから .utilityPanelの 位置を指定して WindowPlacementを返します
この戻り値をif条件でラップし この配置が visionOSの場合のみ 適用されるようにします
これで ウインドウの初回起動時に コントロールが近くに表示されます また プレイヤーは必要に応じて ウインドウを初期位置から動かせます
この新しいコントロールでは まったく新しいやり方で ロボットを操作できます
例えば このボタンをタップすると BOT-anistが手を振ります
visionOSでのコントローラウインドウ のデザインを改善できました 次はmacOSで このウインドウの位置を 手動で計算します
defaultWindowPlacement モディファイアはcontextを提供します プラットフォームによって ここに含まれる情報は異なります macOSでは contextに格納されるのは デフォルトのディスプレイに関する情報です これにアクセスして .visibleRectを取得します これは コンテンツを 問題なく配置できる場所を表します
sizeThatFitsメソッドを使用して ウインドウのコンテンツに基づき 必要となるサイズを参照します
displayBounds変数と size変数を使用して ディスプレイの下端の少し上の 水平方向の中央に表示されるよう 位置を計算します
これで 算出した位置とサイズを WindowPlacementとして返せます
macOSでも コントロールが適切に 配置されるようになりました
プレイヤーはプレイ中に ウインドウの位置を自由に変更でき 別の画面に配置することもできます
優れたウインドウ配置にできました コンテンツが常に 最適に表示されるようにするために ウインドウサイズの変更方法にも 変更を加えましょう
ウインドウには システムにより決定される 初期サイズがあります このデフォルトのサイズは いくつかの方法で変更できます
画面のサイズやほかのウインドウに応じて サイズが決まる場合は defaultWindowPlacement APIを使用して初期サイズを指定できます これは macOSの コントローラウインドウの場合と同様です defaultSizeモディファイアで 初期サイズを変更する方法もあります
なお このデフォルトのサイズは サイズが 別途制約されている場合は使用されません WindowPlacement APIで サイズが指定されている場合や シーンが復元された場合です 先ほど追加した動画ウインドウのように プッシュされるウインドウの場合 defaultSizeは 元のウインドウのサイズと同じになります この例では 元のウインドウは ロボットエディタです
このデフォルトサイズに問題はありませんが プレイヤーは動画ウインドウのサイズを 変更したいかもしれません 動画が常に適切に表示されるように 一定の制限を設けましょう
movieのWindowGroupの .windowResizabilityに .contentSizeを指定すると そのウインドウは 含まれるコンテンツの 最小/最大サイズに基づく制限を受けます MovieContentViewに minWidthとmaxWidth およびminHeightと maxHeightを追加します
これで動画ウインドウのサイズを 最小限にすると正方形になり 拡大も合理的な範囲に 制限されるようになりました
一日中 BOT-anistを見ていられそうです しかし コントロールウインドウに 手を入れる必要があります
非常に大きなサイズに変更可能なので ボリュームの邪魔になります
ウインドウのサイズを そこに含まれる コンテンツのサイズに 合うようにするべきです
movieの WindowGroupの場合と同様に windowResizability モディファイアを controllerの WindowGroupにも追加します
これで コントローラのモードを変更すると コンテンツに合わせて ウインドウのサイズが変わります
このウインドウのサイズを プレイヤーは変更できません 各モードのビューのサイズは固定で 最小/最大サイズが 指定されているのではないためです
BOT-anistは 本当に良くなりましたね visionOSおよびmacOS向けに 素晴らしい改善をアプリに加えられました みなさんのアプリでも ウインドウと それをサポートするAPIをご活用ください
ウインドウと最上位ビューのどちらが アプリに適しているか検討しましょう WindowPlacement APIは 最初のレイアウトを指定する際に有用です コンテンツに合わせてウインドウサイズを 設定し ユーザーによる変更を制限できます
ウインドウに関する プラットフォーム固有の機能により ユーザーによるアプリ利用が より快適になります
ご視聴ありがとうございましたアプリで ウインドウの機能をぜひご活用ください
-
-
2:36 - BOT-anist scenes
@main struct BOTanistApp: App { var body: some Scene { WindowGroup(id: "editor") { EditorContentView() } WindowGroup(id: "game") { GameContentView() } .windowStyle(.volumetric) } }
-
3:09 - Creating the movie WindowGroup
@main struct BOTanistApp: App { var body: some Scene { WindowGroup(id: "editor") { EditorContentView() } WindowGroup(id: "game") { GameContentView() } .windowStyle(.volumetric) WindowGroup(id: "movie") { MovieContentView() } } }
-
3:55 - Opening a movie window
struct EditorContentView: View { @Environment(\.openWindow) private var openWindow var body: some View { Button("Open Movie", systemImage: "tv") { openWindow(id: "movie") } } }
-
4:45 - Pushing a movie window
struct EditorContentView: View { @Environment(\.pushWindow) private var pushWindow var body: some View { Button("Open Movie", systemImage: "tv") { pushWindow(id: "movie") } } }
-
5:34 - Toolbar
CanvasView() .toolbar { ToolbarItem { Button(...) } ... }
-
5:40 - Title menu
CanvasView() .toolbar { ToolbarTitleMenu { Button(...) } ... }
-
5:48 - Hiding window controls
WindowGroup(id: "movie") { ... } .persistentSystemOverlays(.hidden)
-
6:28 - Creating the controller window
@main struct BOTanistApp: App { var body: some Scene { ... WindowGroup(id: "movie") { MovieContentView() } WindowGroup(id: "controller") { ControllerContentView() } } }
-
6:34 - Opening the controller window
struct GameContentView: View { @Environment(\.openWindow) private var openWindow var body: some View { ... Button("Open Controller", systemImage: "gamecontroller.fill") { openWindow(id: "controller") } } }
-
7:46 - Positioning the controller window
WindowGroup(id: "controller") { ControllerContentView() } .defaultWindowPlacement { content, context in #if os(visionOS) return WindowPlacement(.utilityPanel) #elseif os(macOS) ... #endif }
-
8:45 - Positioning the controller window continued
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 }
-
10:12 - Default size
@main struct BOTanistApp: App { var body: some Scene { ... WindowGroup(id: "movie") { MovieContentView() } .defaultSize(width: 1166, height: 680) } }
-
10:49 - Setting resize limits on the movie window
@main struct BOTanistApp: App { var body: some Scene { ... WindowGroup(id: "movie") { MovieContentView() .frame( minWidth: 680, maxWidth: 2720, minHeight: 680, maxHeight: 1020 ) } .windowResizability(.contentSize) } }
-
11:37 - Controller window resizability
@main struct BOTanistApp: App { var body: some Scene { ... WindowGroup(id: "controller") { ControllerContentView() } .windowResizability(.contentSize) } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。