見出し画像

Vision Pro の サンプル解説 (1) - Hello World

以下の記事が面白かったので、簡単にまとめました。

Hello World


1. はじめに

visionOS」の「Volume」や「Immersive」を使用すると、インタラクティブな仮想オブジェクトをユーザーの環境に配置したり、ユーザーを仮想環境に配置したりできます。

「Hello World」は、ユーザーに地球について教えています。地球の傾きが季節をどのように作り出すか、地球の周りを周回する物体がどのように動くか、宇宙から地球がどのように見えるかを示します。

アプリは「SwiftUI」で、2D要素と3D要素の両方を含むインターフェイスを定義します。3Dモデルとエフェクトの作成、カスタマイズ、管理には、「RealityKit」と「Reality Composer Pro」を使います。

2. アプリのエントリーポイント

WindowGroup」で、起動時に表示するシーンを指定します。ウィンドウスタイルは「.plain」です。

WindowGroup("Hello World", id: "modules") {
    Modules()
        .environment(model)
}
.windowStyle(.plain)

visionOSは「WindowGroup」を「ウィンドウ」として表示します。共有スペース内でウィンドウサイズを変更したり、移動したりできます。3Dエクスペリエンスを提供する場合でも、ウィンドウはアプリの出発点として最適です。操作方法を提供するのに適しています。

3. NavigationStack

「Hello World」の紹介アニメーションの後、「Modules」ビューが表示されます。「NavigationStack」のルートには、「TableOfContents」が含まれています。

NavigationStack(path: $model.navigationPath) {
    TableOfContents()
        .navigationDestination(for: Module.self) { module in
            ModuleDetail(module: module)
                .navigationTitle(module.eyebrow)
        }
}

NavigationLink」は、他のプラットフォームと同じ動作をします。 初回はルートビューが表示されます。「NavigationLink」を選択すると、スタックは新しいビューを描画し、ツールバーに「戻る」ボタンで前のビューに戻ります。

navigationDestination(for:destination:) の末尾のクロージャは、対応するリンクがアクティブ化した時にビューを表示します。

NavigationLink(value: module) { /* The link's label. */ }

可能なモジュール値は、カスタムモジュール列挙から取得できます。

enum Module: String, Identifiable, CaseIterable, Equatable {
    case globe, orbit, solar
    // ...
}

4. 地球の表示 - Volume

「Planet Earth」は地球の情報を提供します。

4-1. Information Propaty List

複数のシーンタイプを開くことができるように、Information Property List に UIApplicationSceneManifest キーの UIApplicationSupportsMultipleScenes キーにtrueが指定されています。

<key>UIApplicationSceneManifest</key>
<dict>
    <key>UIApplicationSupportsMultipleScenes</key>
    <true/>
    <key>UISceneConfigurations</key>
    <dict/>
</dict>

4-2. 地球の準備

「WindowGroup」で、地球を表示するシーンを指定します。ウィンドウスタイルは「.volumetric」(3Dの透明な箱のように動作するコンテナ) です。

WindowGroup(id: Module.globe.name) {
    Globe()
        .environment(model)
}
.windowStyle(.volumetric)
.defaultSize(width: 0.6, height: 0.6, depth: 0.6, in: .meters)

defaultSize(width:height:depth:in:) は、奥行き寸法を含むVolumeサイズを指定します。

4-3. 地球の表示

「Globe」には、現在の状態に応じてVolumeを表示・非表示する「View Globe」ボタンが表示されます。

struct GlobeToggle: View {
    @Environment(ViewModel.self) private var model
    @Environment(\.openWindow) private var openWindow
    @Environment(\.dismissWindow) private var dismissWindow


    var body: some View {
        @Bindable var model = model


        Toggle(Module.globe.callToAction, isOn: $model.isShowingGlobe)
            .onChange(of: model.isShowingGlobe) { _, isShowing in
                if isShowing {
                    openWindow(id: Module.globe.name)
                } else {
                    dismissWindow(id: Module.globe.name)
                }
            }
            .toggleStyle(.button)
    }
}

5. 地球・月・人工衛星の表示 - Immersive

5-1. 地球・月・人工衛星の準備

「Objects in Orbit」は、月や人工衛星など、地球の周りを回る物体に関する情報を提供します。

ItemView 内の Model3D 構造を使用して、アセットバンドルからこれらのモデルを読み込みます。 ビューは利用可能な空間に合わせてモデルを拡大縮小して配置し、オプションの方向調整を適用します。

private struct ItemView: View {
    var item: Item
    var orientation: SIMD3<Double> = .zero


    var body: some View {
        Model3D(named: item.name, bundle: worldAssetsBundle) { model in
            model.resizable()
                .scaledToFit()
                .rotation3DEffect(
                    Rotation3D(
                        eulerAngles: .init(angles: orientation, order: .xyz)
                    )
                )
                .frame(depth: modelDepth)
                .offset(z: -modelDepth / 2)
        } placeholder: {
            ProgressView()
                .offset(z: -modelDepth * 0.75)
        }
    }
}

ItemViewをモデルごとに1回使用し、現在の選択に基づいてのみ表示されるオーバーレイにそれぞれを配置します。

.overlay {
    ItemView(item: .satellite, orientation: [0.15, 0, 0.15])
        .opacity(selection == .satellite ? 1 : 0)
}

モデルを含む VStack には、表示するモデルを選択するために使用する Picker も含まれています。

Picker("Satellite", selection: $selection) {
    ForEach(Item.allCases) { item in
        Text(item.name)
    }
}
.pickerStyle(.segmented)

2Dウィンドウに3D効果を追加するときは、次のガイダンスに留意してください。

・やりすぎない
さまざまな方向からウィンドウを見ると、重要なコントロールや情報が意図せず見えにくくなる場合があります。

・要素が利用可能な深さを超えないように
深さが過剰になると要素がクリップされる原因になります。

・背面ガラスと交差するモデルは避ける
初期配置後の潜在的な移動を考慮してください。

5-2. 地球・月・人工衛星の表示

地球、月、人口衛星を一緒に表示するため、衛星が地球の周りをどのように移動するかを視覚的に確認できます。

アプリは、「.mixed」スタイルの「ImmersiveSpace」シーンに、システム全体をモデル化する単一のRealityViewを含む「Orbit」ビューを配置します。

ImmersiveSpace(id: Module.orbit.name) {
    Orbit()
        .environment(model)
}
.immersionStyle(selection: $orbitImmersionStyle, in: .mixed)

このシーンは、Information Property List の UIApplicationSceneManifest キーの有無に依存します。

struct OrbitToggle: View {
    @Environment(ViewModel.self) private var model
    @Environment(\.openImmersiveSpace) private var openImmersiveSpace
    @Environment(\.dismissImmersiveSpace) private var dismissImmersiveSpace


    var body: some View {
        @Bindable var model = model


        Toggle(Module.orbit.callToAction, isOn: $model.isShowingOrbit)
            .onChange(of: model.isShowingOrbit) { _, isShowing in
                Task {
                    if isShowing {
                        await openImmersiveSpace(id: Module.orbit.name)
                    } else {
                        await dismissImmersiveSpace()
                    }
                }
            }
            .toggleStyle(.button)
    }
}

「地球の表示」のバージョンとは、いくつかの重要な違いがあります。

・OrbitToggle はウィンドウではなく、環境から openImmersiveSpace と dismissImmersiveSpace を使用。
・この場合の閉じるアクションには識別子は必要ない。
・空間をオープンとクローズは非同期で動作するため、タスク内に表示される。

6. 太陽系の表示 - Full Immersive

「The Solar System」は、太陽系の情報を提供します。ユーザーがボタンをタップすると、アプリがディスプレイ全体を引き継ぎ、全方向に星を表示します。 終了するための小さなコントロール パネルも表示されます。

ここではパススルー ビデオをオフにした完全なImmersiveStyleを使用しています。

ImmersiveSpace(id: Module.solar.name) {
    SolarSystem()
        .environment(model)
}
.immersionStyle(selection: $solarImmersionStyle, in: .full)

このシーンは、SolarSystemToggle によってアクティブ化されます。

struct SolarSystemToggle: View {
    @Environment(ViewModel.self) private var model
    @Environment(\.openImmersiveSpace) private var openImmersiveSpace
    @Environment(\.dismissImmersiveSpace) private var dismissImmersiveSpace


    var body: some View {
        Button {
            Task {
                if model.isShowingSolar {
                    await dismissImmersiveSpace()
                } else {
                    await openImmersiveSpace(id: Module.solar.name)
                }
            }
        } label: {
            if model.isShowingSolar {
                Label(
                    "Exit the Solar System",
                    systemImage: "arrow.down.right.and.arrow.up.left")
            } else {
                Text(Module.solar.callToAction)
            }
        }
    }
}

このコントロールは、Full Immersiveエクスペリエンスを開始する方法を提供するためにメインウィンドウに表示され、終了する方法としてコントロールパネルに個別に表示されます。

太陽系コントロールのメイン ウィンドウを再利用するために、NavigationStackとControlの両方を ZStack に配置し、一度に1つだけが表示されるようにそれぞれの不透明度を設定します。

ZStack {
    SolarSystemControls()
        .opacity(model.isShowingSolar ? 1 : 0)


    NavigationStack(path: $model.navigationPath) {
        // ...
    }
    .opacity(model.isShowingSolar ? 0 : 1)
}
.animation(.default, value: model.isShowingSolar)

次回



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