見出し画像

Full Spaceからの脱出

はじめに

visionOSで空間を覆う際は、Immersive Spaceを使用します。しかし、360度全ての空間をカバーして没入型の体験を提供する場合、ユーザーがその空間から脱出するための明確な出口の表現が必要です。AppleもWWDCの動画で、この点に何度も言及しています。

今回、Immersive Spaceからの脱出方法について、「Hello World」を例に実装の解説を行います。調査を始める前は、Immersive Spaceから簡単に脱出できる何かが提供されていると思っていました。しかし、実際には手順が多く必要だったのです。「Hello World」からImmersive Spaceを脱出する部分のみをGitHubに公開していますので、参考にしてください。

https://github.com/tochi/ImmersiveSpaceSample

Hello Worldの実装を読み解く

Hello Worldと同じNavigationStackを利用した場合

「Hello World」では、「Show Immersive Space」ボタンを押すことで、没入型空間のImmersive Spaceが表示されます。この空間内には、ユーザーが空間から脱出するための「Exit Immersive Space」ボタンが用意されており、空間上には小さなウィンドウとして表示されます。そして、「Exit Immersive Space」ボタンを押すと、ユーザーは元の画面に戻ることができます。この仕組みは、ユーザーに没入型空間を提供しつつ、空間の邪魔にならないよう小さなウィンドウを配置し、簡単に通常のパススルー状態へ戻ることが可能です。

# ImmersiveSpaceSampleApp.swift
ZStack {
  ControlPanelView()
    .environment(viewModel)
    .opacity(viewModel.immersiveSpaceIsShown ? 1 : 0)
    
  NavigationStack {
    ContentView(title: "Hello world !")
      .environment(viewModel)
  }
  .opacity(viewModel.immersiveSpaceIsShown ? 0 : 1)
}
.animation(.default, value: viewModel.immersiveSpaceIsShown)

ImmersiveSpaceSampleAppを詳しく見てみましょう。ControlPanelViewは、Immersive Space内に「Exit Immersive Space」ボタンを表示するための小さなビューです。NavigationStack内のContentViewは、「Show Immersive Space」ボタンを表示するためのビューです。ZStackを使用して、これらのビューを重ね合わせて表示しています。

重要なポイントは、これらのビューの透明度をopacityプロパティで設定し、viewModel.immersiveSpaceIsShownの状態に応じて透明度を制御していることです。Immersive Spaceが表示されている際には、「Show Immersive Space」ボタンを含むNavigationStackは非表示になり、「Exit Immersive Space」ボタンを含むControlPanelViewが表示されます。逆に、Immersive Spaceから退出すると、NavigationStackが表示されControlPanelViewが非表示になります。

  • Immersive Spaceを表示するとき:

    • NavigationStack(「Show Immersive Space」ボタンを含む)は非表示

    • ControlPanelView(「Exit Immersive Space」ボタンを含む)は表示

  • Immersive Spaceを非表示にするとき:

    • NavigationStack(「Show Immersive Space」ボタンを含む)は表示

    • ControlPanelView(「Exit Immersive Space」ボタンを含む)は非表示

さらに、これらのビューを内包するZStackanimation(.default, value: viewModel.immersiveSpaceIsShown)を設定することで、ビューの切り替えが自然なアニメーションで表現されます。

# ImmersiveSpaceSampleApp.swift
WindowGroup(id: "Main") {
  ...
}
.windowStyle(.plain)

また、WindowGroupwindowStyle(.plain)を設定することも重要なポイントです。この設定により、配下のビューにおけるガラス窓(Glass window)の表現がなくなります。ただし、内包されているNavigationStackに関しては、ガラス窓の表現が維持されます。

# ControlPanelView.swift
VStack {
  Spacer()
  VStack {
    ...
  }
  .frame(width: 400)
  .glassBackgroundEffect(in: .rect(cornerRadius: 40))
}

「Exit Immersive Space」ボタンを含むControlPanelViewについて見てみましょう。呼び出し元のImmersiveSpaceSampleAppWindowGroupwindowStyle(.plain)が設定されているため、ControlPanelViewではガラス窓(Glass window)の表現が消えてしまいます。しかし、ControlPanelViewではGlass windowの効果を活用したいため、glassBackgroundEffectを使用して手動で追加しています。

visionOSでは、ウィンドウをドラッグしたり閉じたりするためのアクションがウィンドウの一番下に配置されています。これらのアクションとGlass windowを近接させて配置したいため、Glass windowが設置されたVStackの上にSpacerを置き、ウィンドウを下部に寄せています。Spacerは余白を生成し、それによってその下に配置された要素を画面の下部に押し下げる役割を果たしています。

# ImmersiveView.swift
RealityView { content in
  ...
}
.onAppear {
  viewModel.immersiveSpaceIsShown = true
}
.onDisappear {
  viewModel.immersiveSpaceIsShown = false
}

ImmersiveViewを見ると、onAppearonDisappearにおいてviewModel.immersiveSpaceIsShownの状態更新を行っています。これは、Immersive Spaceの切り替えアニメーションをスムーズに表現するために、ライフサイクルイベントを活用して状態を変更しているものです。

TabViewではOrnamentが消えない

TabViewではOpacityを0にしてもOrnamentが消えない

それでは、Immersive spaceを利用するアプリで複数画面の切り替えを考慮する必要がある場合にはどの様にしますか?visionOSで新たに追加されたOrnament表現を利用するためにTabViewを使用するのは自然な選択です。TabViewを利用することで、ユーザーはウィンドウの横に表示されるタブをタップするだけで、画面を簡単に切り替えることが可能になります。

# ImmersiveSpaceSampleApp.swift
WindowGroup(id: "Main") {
  ZStack {
    ControlPanelView()
      .environment(viewModel)
      .opacity(viewModel.immersiveSpaceIsShown ? 1 : 0)
    TabView {
      ...
    }
    .frame(width: 1280)
    .glassBackgroundEffect(in: .rect(cornerRadius: 40))
    .opacity(viewModel.immersiveSpaceIsShown ? 0 : 1)
  }
  .animation(.default, value: viewModel.immersiveSpaceIsShown)
}
.windowStyle(.plain)

しかし、Immersive Spaceの表現を行うアプリケーションにおいては、TabViewの使用は適していません。その理由は、TabViewの透明度(opacity)を変更しても、タブアイテム(Ornament)が非表示にならないためです。加えて、TabViewではNavigationStackのようなGlass windowが適用されません。

複数画面の構成ではSegmentedなどで工夫する

PickerのSegmentedPickerStyleを利用して画面切り替え

Immersive Spaceのアプリケーションで複数画面を切り替えたい場合にはPickerSegmentedPickerStyleとして使用するのが良さそうです。Pickerはユーザーに複数の選択肢から一つを選んでもらうためのコントロールですが、SegmentedPickerStyleを適用することで、異なるビューやコンテンツ間を簡単に切り替えることが可能になります。

# ImmersiveSpaceSampleApp.swift
WindowGroup(id: "Main") {
  ZStack {
    ControlPanelView()
      .environment(viewModel)
      .opacity(viewModel.immersiveSpaceIsShown ? 1 : 0)
    SegmentedView()
      .environment(viewModel)
      .opacity(viewModel.immersiveSpaceIsShown ? 0 : 1)
  }
  .animation(.default, value: viewModel.immersiveSpaceIsShown)
}
.windowStyle(.plain)

「Hello World」のアプリケーションにおいて、Navigation Stackの代わりにSegmentedViewを用意し、その透明度(opacity)を設定することで、Immersive paceの表示状態に応じてViewの表示を切り替えることができます。この方法により、Immersive Spaceがアクティブなときと非アクティブなときで、ユーザーインターフェースの要素を適切に表示または非表示にすることが可能になります。

# SegmentedView.swift
VStack {
  Picker("", selection: $selectionIndex) {
    ForEach(0..<viewTitles.count) { index in
      Text(viewTitles[index])
        .tag(index)
    }
  }
  .pickerStyle(SegmentedPickerStyle())
  .frame(width: 1200)
  ...
}
.frame(width: 1280)
.padding(EdgeInsets(top: 40, leading: 0, bottom: 40, trailing: 0))
.glassBackgroundEffect(in: .rect(cornerRadius: 40))

SegmentedViewでは、ControlPanelViewと同様にglassBackgroundEffectを使用して、visionOS特有のGlass windowを適用します。その後、表示したいViewを切り替えるためにPickerを使い、pickerStyle(SegmentedPickerStyle())を指定してSegmented pickerの表示を行います。これにより、ユーザーは複数の選択肢の中から簡単に画面を選び、切り替えることができます。

まとめ

ちなみに、この実装を調べる中でWindowGroupを複数準備して画面を切り替える方法も試みましたが、ウィンドウの位置がずれるという問題が発生し、この方法は採用しないことにしました。結局、「Hello world」などのAppleが提供しているサンプルコードの実装は現時点でベストプラクティスなようです。
Immersive Spaceに小さなコントローラーウィンドウを表示する実装については、当初の想定よりも手順が多いと感じました。少しでも参考になれば幸いです。

参考


この記事が気に入ったらサポートをしてみませんか?