VisionOS沉浸式空间加载动画实现及AVPlayer动态URL传递方案
VisionOS沉浸式视图:视频加载动画实现与动态URL传递
一、传递动态URL到沉浸式视图
因为沉浸式视图由App结构体初始化,可通过以下两种方式传递动态URL:
方式1:利用ImmersiveSpace的session参数传递
在App结构体中定义沉浸式空间时,通过session.extra接收外部传入的URL:
@main struct MeditationApp: App { var body: some Scene { WindowGroup { HomeView() // 首页视图,触发打开沉浸式空间 } ImmersiveSpace(id: "MeditationPlayer") { session in // 从session参数中提取动态URL if let videoURL = session.extra?["videoURL"] as? URL { ImmersiveMeditationPlayerView(videoURL: videoURL) } } } }
在触发打开沉浸式空间的视图(如首页)中,传入目标URL:
struct HomeView: View { @Environment(\.openImmersiveSpace) private var openImmersiveSpace var body: some View { Button("启动沉浸式冥想") { // 替换为实际动态获取的URL guard let dynamicURL = URL(string: "https://your-video-url.m3u8") else { return } Task { try? await openImmersiveSpace(id: "MeditationPlayer", extra: ["videoURL": dynamicURL]) } } } }
方式2:App全局状态管理
通过App内的@State变量存储动态URL,沉浸式视图监听该变量:
@main struct MeditationApp: App { @State private var targetVideoURL: URL? @State private var immersiveSpaceID: ImmersiveSpace.ID? var body: some Scene { WindowGroup { HomeView() .environment(\.setVideoURL) { url in targetVideoURL = url Task { immersiveSpaceID = try? await openImmersiveSpace(id: "MeditationPlayer") } } } ImmersiveSpace(id: "MeditationPlayer") { if let url = targetVideoURL { ImmersiveMeditationPlayerView(videoURL: url) } } } } // 自定义环境键传递URL设置方法 private struct SetVideoURLKey: EnvironmentKey { static let defaultValue: (URL) -> Void = { _ in } } extension EnvironmentValues { var setVideoURL: (URL) -> Void { get { self[SetVideoURLKey.self] } set { self[SetVideoURLKey.self] = newValue } } }
二、实现视频加载完成前的加载动画
通过监听AVPlayerItem的加载状态,控制沉浸式视图中加载动画的显示/隐藏:
1. 定义视图模型管理播放状态
import AVFoundation import Observation @Observable class VideoPlayerViewModel { var isLoading = true let player = AVPlayer() private var playerItem: AVPlayerItem? private var observation: NSKeyValueObservation? func setupPlayer(with url: URL) { let asset = AVURLAsset(url: url) playerItem = AVPlayerItem(asset: asset) // 监听播放项状态变化 observation = playerItem?.observe(\.status, options: [.initial, .new]) { [weak self] item, change in guard let self = self else { return } DispatchQueue.main.async { switch item.status { case .readyToPlay: self.isLoading = false case .failed: self.isLoading = false print("视频加载失败: \(item.error?.localizedDescription ?? "未知错误")") default: break } } } player.replaceCurrentItem(with: playerItem) player.play() } }
2. 修改沉浸式视图添加加载动画
import SwiftUI import RealityKit import AVFoundation struct ImmersiveMeditationPlayerView: View { @Environment(\.dismissImmersiveSpace) private var dismissImmersiveSpace let videoURL: URL @State private var viewModel = VideoPlayerViewModel() var body: some View { VStack { RealityView { content, attachments in // 1. 创建视频球实体 let videoEntity = Entity() let videoMaterial = VideoMaterial(avPlayer: viewModel.player) videoEntity.components.set(ModelComponent( mesh: .generateSphere(radius: 1000), materials: [videoMaterial] )) // 调整视频球属性(修正原代码旋转轴错误) videoEntity.scale = .init(x: 1, y: 1, z: -1) videoEntity.transform.translation = SIMD3<Float>(0, 10, 0) let rotation = simd_quatf(angle: Float(Angle.degrees(90).radians), axis: .init(x: 1, 0, 0)) videoEntity.transform.rotation = rotation content.add(videoEntity) // 2. 创建加载动画实体(旋转立方体) let loadingEntity = Entity() let loadingMesh = MeshResource.generateBox(size: 0.5) let loadingMaterial = SimpleMaterial(color: .white, isMetallic: true) loadingEntity.components.set(ModelComponent(mesh: loadingMesh, materials: [loadingMaterial])) loadingEntity.transform.translation = SIMD3<Float>(0, 10, 0) content.add(loadingEntity) attachments.set(loadingEntity, for: "loading") // 3. 绑定加载状态控制动画 viewModel.$isLoading .receive(on: DispatchQueue.main) .sink { isLoading in guard let loadingEntity = attachments.entity(for: "loading") else { return } if isLoading { // 启动无限旋转动画 loadingEntity.move( to: Transform(rotation: simd_quatf(angle: .pi * 2, axis: .y)), relativeTo: loadingEntity.parent, duration: 2, timingFunction: .linear ) loadingEntity.repeatAnimation(.infinity) } else { // 加载完成后淡出移除动画 withAnimation(.easeOut(duration: 0.5)) { loadingEntity.scale = .zero } completion: { loadingEntity.removeFromParent() } } } .store(in: &attachments.cancellables) } .onAppear { viewModel.setupPlayer(with: videoURL) } } } }
关键说明
- 使用
@Observable视图模型管理播放状态,避免直接在视图中处理KVO逻辑 - 加载动画采用RealityKit实体实现,保持沉浸式体验一致性
- 监听
AVPlayerItem.status状态,准确判断视频是否准备就绪
内容的提问来源于stack exchange,提问作者Ahsan Ali
相关产品推荐
相关产品推荐

