WWDC Quick Look 💓 By SwiftGGTeam
Go beyond the window with SwiftUI

Go beyond the window with SwiftUI

元の動画を見る

ハイライト

SwiftUI は、WindowGroup および Volume と並んで、visionOS に ImmersiveSpace シーン タイプを導入し、アプリケーション コンテンツがウィンドウの境界を突破し、openImmersiveSpace/dismissImmersiveSpace 環境アクションを通じて出入りを制御できるようにします。 RealityView および ARKit と連携して、複合現実から完全な没入感まで、複数の体験レベルを実現します。

主な内容

3 つのコンテナ、3 つのエクスペリエンス レベル

visionOS 上の SwiftUI は、従来のウィンドウ用の WindowGroup、境界のある 3D コンテンツ用の Volume、境界のない空間エクスペリエンス用の ImmersiveSpace の 3 つのシーン コンテナーを提供します。最初の 2 つはコンテンツをコンテナー内に限定しますが、ImmersiveSpace ではユーザーの周囲のどこにでもコンテンツを表示できます。

アプリが ImmersiveSpace を開くと、フルスペース モードになります。他のすべてのアプリを非表示にして、コンテンツ用のスペースを確保します。終了後、他のアプリケーションは自動的に再開されます。アプリで同時に開くことができるスペースは 1 つだけです。

(03:00)

ImmersiveSpace を定義して開く

ImmersiveSpace の定義には、数行のコードのみが必要です。

@main
struct WorldApp: App {
    var body: some Scene {
        ImmersiveSpace(id: "solar") {
            SolarSystem()
        }
    }
}

キーポイント:

  • ImmersiveSpace は、WindowGroup に似た新しいシーン タイプです。
  • id パラメーターを使用例してスペースに名前を付け、その後、この識別子を使用例してスペースを開きます
  • スペースの原点はユーザーの足元にあり、コンテンツの配置はこの位置を基準にして考慮する必要があります。

ウィンドウからスペースを開くには、環境アクションが必要です。

struct SpaceControl: View {
    @Environment(\.openImmersiveSpace) private var openImmersiveSpace
    @Environment(\.dismissImmersiveSpace) private var dismissImmersiveSpace
    @State private var isSpaceHidden: Bool = true

    var body: some View {
        Button(isSpaceHidden ? "View Outer Space" : "Exit the solar system") {
            Task {
                if isSpaceHidden {
                    let result = await openImmersiveSpace(id: "solar")
                    switch result {
                    case .opened:
                        isSpaceHidden = false
                    case .error:
                        // オープン失敗を処理
                        break
                    default:
                        break
                    }
                } else {
                    await dismissImmersiveSpace()
                    isSpaceHidden = true
                }
            }
        }
    }
}

キーポイント:

  • openImmersiveSpacedismissImmersiveSpace は、@Environment を通じて取得される環境値です。
  • openImmersiveSpace(id:) は非同期結果を返し、成功と失敗の両方の状況を処理する必要があります。
  • dismissImmersiveSpace() は、同時に開くことができるスペースは 1 つだけであるため、パラメーターは必要ありません
  • これらのアクションは非同期であり、システムはスペースの出入りアニメーションが完了した後に戻ります。

(09:11)

RealityView と ImmersiveSpace の連携

ImmersiveSpace と RealityView は連携して動作するように設計されています。 RealityView は、非同期読み込み、ARKit アンカー ポイントの配置、手と頭の姿勢データ アクセス、およびその他の機能を提供します。

ImmersiveSpace {
    RealityView { content in
        let starfield = await loadStarfield()
        content.add(starfield)
    }
}

キーポイント:

  • RealityView のクロージャは非同期をサポートし、3D リソースを非同期でロードできます
  • 読み込み完了後、content.add() を通じてエンティティをシーンに追加します
  • RealityView はレンダリングに RealityKit を使用例しており、座標系は SwiftUI とは異なります

座標系の違いに注意してください。

  • SwiftUI では、y 軸は下向き、z 軸はユーザーに向かって向きます。
  • RealityKit では y 軸が上向き
  • RealityView でエンティティを配置するときは、RealityKit の座標規則に従います。

(06:53)

##詳細

マルチシナリオのアプリケーション構造

典型的なvisionOSアプリケーションには、ウィンドウとスペースの両方が含まれています。

@main
struct WorldApp: App {
    var body: some Scene {
        // 最初のシーンは起動時に表示されるウィンドウ
        WindowGroup {
            VStack {
                Text("The Solar System")
                    .font(.largeTitle)
                SpaceControl()
            }
        }

        // ImmersiveSpace はデフォルトでは表示されず、手動で開く必要がある
        ImmersiveSpace(id: "solar") {
            SolarSystem()
        }
    }
}

キーポイント:

  • アプリ起動時にシーンリストの最初のシーンが自動表示されます
  • ImmersiveSpace はデフォルトでは非表示になっていますが、ボタンなどのインタラクション トリガーを通じて開くことができます。
  • Space に直接起動するようにアプリを設定することもできます (下記を参照)

(10:44)

3D モデルの非同期読み込み

3D リソースの読み込みには時間がかかりますが、Model3D は読み込みステータス管理を提供します。

Model3D(named: "Earth") { phase in
    switch phase {
    case .empty:
        Text("Waiting")
    case .failure(let error):
        Text("Error \(error.localizedDescription)")
    case .success(let model):
        model.resizable()
    }
}

キーポイント:

  • phase パラメータは読み込みステータスを反映します: 空、失敗、成功
  • 空の状態ではロード命令を表示し、失敗状態ではエラーメッセージを表示します
  • 成功するとロードされたモデルを取得します。.resizable() などの修飾子を適用できます。

(11:32)

シーンフェーズ管理

ImmersiveSpace は SwiftUI のシーン フェーズをサポートし、アクティブな状態の変化に応答できます。

@main
struct WorldApp: App {
    @EnvironmentObject private var model: ViewModel
    @Environment(\.scenePhase) private var scenePhase

    var body: some Scene {
        ImmersiveSpace(id: "solar") {
            SolarSystem()
                .onChange(of: scenePhase) {
                    switch scenePhase {
                    case .inactive, .background:
                        model.solarEarth.scale = 0.5
                    case .active:
                        model.solarEarth.scale = 1.0
                    default:
                        break
                    }
                }
        }
    }
}

キーポイント:

  • ユーザーが境界線から出るか、システムアラートがポップアップすると、スペースは非アクティブ状態になります。
  • ユーザーは体験エリアに戻った後、アクティブ状態に戻ります
  • シーンのフェーズ変更を使用例して、コンテンツを縮小してステータスの変更を促すなど、視覚的なフィードバックを提供します

(13:04)

座標変換

ウィンドウとスペースはそれぞれ独立した座標系を持っています。 SwiftUI は、変換用の ImmersiveSpace 座標空間を提供します。

GeometryReader3D { proxy in
    Earth()
        .onTapGesture {
            if let translation = proxy.transform(in: .immersiveSpace)?.translation {
                model.solarEarth.position = Point3D(translation)
            }
        }
}

キーポイント:

  • GeometryReader3D は 3D ジオメトリ情報を提供します
  • proxy.transform(in: .immersiveSpace) ImmersiveSpace 座標系での現在のビューの変換を取得します
  • ウィンドウ内のクリック位置を変換により空間上のエンティティの位置にマッピングできます

(14:21)

3 つの没入スタイル

visionOS は、コンテンツが周囲の環境をどのように引き継ぐかを制御する 3 つの没入スタイルを提供します。

@main
struct WorldApp: App {
    @State private var currentStyle: ImmersionStyle = .mixed

    var body: some Scene {
        ImmersiveSpace(id: "solar") {
            SolarSystem()
                .simultaneousGesture(
                    MagnifyGesture()
                        .onChanged { value in
                            let scale = value.magnification
                            if scale > 10 {
                                currentStyle = .full
                            } else if scale > 5 {
                                currentStyle = .progressive
                            } else {
                                currentStyle = .mixed
                            }
                        }
                )
        }
        .immersionStyle(selection: $currentStyle, in: .mixed, .progressive, .full)
    }
}

キーポイント:

  • .mixed (デフォルト): コンテンツは現実の環境と共存し、周囲のすべてが表示されます。
  • .progressive: コンテンツは前方ポータルに表示され、Digital Crown で没入感を調整可能
  • .full: 視野を完全に引き継ぎ、ユーザーは仮想コンテンツに完全に没入します。
  • .immersionStyle(selection:in:) 修飾子宣言でサポートされるスタイルのリスト
  • ユーザーはデジタルクラウンを通じてプログレッシブスタイルでパススルー比を調整できます

(16:34)

環境への影響と手の隠蔽

プログレッシブ スタイルとフル スタイルでは、エクスペリエンスをさらに調整できます。

ImmersiveSpace(id: "solar") {
    SolarSystem()
        .preferredSurroundingsEffect(.systemDark)
}
.immersionStyle(selection: $currentStyle, in: .progressive)

キーポイント:

  • .preferredSurroundingsEffect(.systemDark) は周囲の環境を暗くし、スペースの内容を強調表示します。
  • プログレッシブ スタイルとフル スタイルでのみ有効です

本物の手を非表示にし、完全なスタイルで仮想の手を表示します。

ImmersiveSpace(id: "solar") {
    SolarSystem()
}
.immersionStyle(selection: $currentStyle, in: .full)
.upperLimbVisibility(.hidden)

キーポイント:

  • .upperLimbVisibility(.hidden) 完全な没入状態で実際の手を非表示にします
  • ARKit ハンド トラッキングと併用して仮想ハンド モデルを表示します

(20:08)

ARKit ハンドトラッキングと仮想手袋

ARKit を通じて手のスケルトン データを取得して、仮想グローブ モデルを駆動します。

struct SpaceGloves: View {
    let arSession = ARKitSession()
    let handTracking = HandTrackingProvider()

    var body: some View {
        RealityView { content in
            let root = Entity()
            content.add(root)

            let leftGlove = try! Entity.loadModel(named: "assets/gloves/LeftGlove_v001.usdz")
            root.addChild(leftGlove)

            let rightGlove = try! Entity.loadModel(named: "assets/gloves/RightGlove_v001.usdz")
            root.addChild(rightGlove)

            Task {
                do {
                    try await arSession.run([handTracking])
                } catch let error as ProviderError {
                    print("Provider error: \(error.localizedDescription)")
                } catch {
                    print("Unexpected error: \(error.localizedDescription)")
                }

                for await anchorUpdate in handTracking.anchorUpdates {
                    let anchor = anchorUpdate.anchor
                    switch anchor.chirality {
                    case .left:
                        leftGlove.transform = Transform(matrix: anchor.transform)
                        for (index, jointName) in anchor.skeleton.definition.jointNames.enumerated() {
                            leftGlove.jointTransforms[index].rotation = simd_quatf(
                                anchor.skeleton.joint(named: jointName).localTransform
                            )
                        }
                    case .right:
                        rightGlove.transform = Transform(matrix: anchor.transform)
                        for (index, jointName) in anchor.skeleton.definition.jointNames.enumerated() {
                            rightGlove.jointTransforms[index].rotation = simd_quatf(
                                anchor.skeleton.joint(named: jointName).localTransform
                            )
                        }
                    }
                }
            }
        }
    }
}

キーポイント:

  • ARKitSessionHandTrackingProvider は ARKit のコア タイプです
  • arSession.run([handTracking]) ハンドトラッキングを有効にする
  • handTracking.anchorUpdates は、非同期ハンド アンカー ポイント更新ストリームを提供します
  • anchor.chirality 左手と右手を区別する
  • anchor.skeleton.jointNamesjoint(named:).localTransform は各指の関節の変形を取得します
  • 関節の回転を仮想モデルの jointTransforms に同期させて、手袋が実際の手の動きに追従できるようにします

(20:52)

宇宙へ直接出発

完全に没入型のアプリケーション (ゲームなど) の場合、起動時に直接スペースに入るようにアプリケーションを構成できます。

@main
struct WorldApp: App {
    var body: some Scene {
        ImmersiveSpace(id: "solar") {
            SolarSystem()
        }
        .immersionStyle(selection: .constant(.full), in: .full)
    }
}

Info.plist で UIApplicationSceneManifest を構成し、ImmersiveSpace を起動シーンとして設定する必要があります。

(19:33)

重要なポイント

  • やること: 窓から没入型の宇宙体験に入ることができる星空観察アプリを作成します

    • 䡡価値: ImmersiveSpace を使用例すると、アプリケーションは通常の 2D ウィンドウから 360 度の星空環境にスムーズに移行できます。ユーザーはまず天文情報を閲覧し、クリックして没入モードに入り、太陽系を探索します。
    • Haikata: WindowGroup を入り口として定義し、3D コンテンツを保存するために ImmersiveSpace を定義し、openImmersiveSpace(id:) を使用例して切り替えをトリガーします。
  • やること: プログレッシブイマーシブモードの仮想トレーニンググラウンドをフィットネスアプリケーションに追加します

    • 䡡価値: 先進的なスタイルにより、ユーザーは環境意識を維持しながら没入型トレーニングを楽しむことができます。 「リビングルームで仮想トレーナーを見る」から「完全に仮想ジムに入る」まで、Digital Crown で没入感を調整します
    • Hajime: .immersionStyle(selection:in:) を使用例してミックスとプログレッシブをサポートし、MagnifyGesture またはボタンを使用例してスタイルを切り替えます
  • やること: ハンドトラッキングをサポートする仮想楽器演奏アプリケーションを開発する

    • 䡡値: ARKit ハンド トラッキングは、演奏の動きを検出するために各指の関節に関する正確なデータを提供します。 .upperLimbVisibility(.hidden) と仮想ハンド モデルを組み合わせて、リアルな演奏感を実現
    • Hajime Kata: HandTrackingProvider を使用例して手のアンカー ポイントを取得し、関節変換を仮想楽器モデルのインタラクション エリアにマッピングします。
  • やること: 複数人コラボレーションの 3D ホワイトボード アプリケーションを構築する

    • 䡡価値: ImmersiveSpace は SharePlay をサポートしており、複数のユーザーが同じスペースで同じコンテンツを表示および操作できます。座標変換 API により、ウィンドウ内の 2D コンテンツを 3D 空間に正確に配置できます。
    • ** はじめ **: GroupActivities フレームワークを統合し、位置変換に GeometryReader3D および .immersiveSpace 座標空間を使用例します
  • やること: 直接起動できる瞑想/リラクゼーション アプリを作成する

    • 䡡価値: 完全没入スタイルは外部干渉を完全にブロックし、.preferredSurroundingsEffect(.systemDark) で集中した雰囲気を作り出すことができます。エクスペリエンスは、アプリケーションの起動後すぐに開始され、ユーザーによる追加の操作は必要ありません。
    • Hajime: アプリ宣言では ImmersiveSpace のみを保持し、immersionStyle を full に設定し、直接開始するようにシーン マニフェストを構成します

関連セッション

コメント

GitHub Issues · utterances