[VisionOS] 拆分HelloWorld的功能点 - 沉浸式与窗口之间的切换

1,458 阅读3分钟

背景

在体验HelloWorld时,很好奇每个功能是怎么实现的,但是这个demo复用了很多功能、数据模型,刚开始理解起来就比较困难。所以我就先从功能点来看,将复用的功能、数据模型都剔除掉,保证单一功能能解藕单独运行。

环境

Xcode:15.1 beta

VisionOS:1.0

梳理功能

graph LR;
    功能点-->A(设置光照);
    style A fill:#bbf,color:#fff
    click A "https://juejin.cn/post/7298690615046651943"
    
    功能点-->B(手势转动地球)
    style B fill:#bbf,color:#fff
    click B "https://juejin.cn/post/7298765809290706983"
    
    功能点-->C(地球自转)
    style C fill:#bbf,color:#fff
    click C "https://juejin.cn/post/7298775642261569575"
    
    功能点-->D(地球跟随鼠标拖动)
    style D fill:#bbf,color:#fff
    click D "https://juejin.cn/post/7299037876637351975"
    
    功能点-->E(卫星围绕地球转动)
    style E fill:#bbf,color:#fff
    click E "https://juejin.cn/post/7300431522255241250"
    
    功能点-->G(沉浸式与窗口之间的切换)
    style G fill:#bbf,color:#fff
    click G "https://juejin.cn/spost/7300816733525901352"

沉浸式与窗口之间的切换

Nov-13-2023 19-22-22.gif

1. 打开沉浸式空间

import SwiftUI

@main
struct MyWorldApp: App {
    @State private var solarImmersionStyle: ImmersionStyle = .full

    @State private var model = ViewModel()

    var body: some Scene {
        // 1.不带边框的window
        WindowGroup{
            SolarDisplayView()
                .environment(model)
        }
        .windowStyle(.plain)
        
        // 2.沉浸式空间
        ImmersiveSpace(id: Module.solar.name) {
            SolarView()
                .environment(model)
        }
        .immersionStyle(selection: $solarImmersionStyle, in: .full)
    }
    
    // 3. 注册的Component、System
    init() {
        RotationComponent.registerComponent()
        RotationSystem.registerSystem()
        TraceComponent.registerComponent()
        TraceSystem.registerSystem()
    }
}

1.1 定义沉浸式空间

在打开沉浸式空间之前,我们需要定义一个沉浸式空间,在App里面作为Scene返回。

ImmersiveSpace(id: Module.solar.name) {
    SolarView()
        .environment(model)
}

1.2 打开/关闭空间

// 打开
@Environment(\.openImmersiveSpace) private var openImmersiveSpace
Task {
    await openImmersiveSpace(id: Module.solar.name)
}
// 关闭
@Environment(\.dismissImmersiveSpace) private var dismissImmersiveSpace
Task {
    await dismissImmersiveSpace()
}

注意: 我们在打开空间openImmersiveSpace后,之前的window并不会消失,还会一直保留。也就是说,即使你进入了沉浸式空间后,之前的window也会进入沉浸式空间,同一时间会存在一个window、一个space。

为了更多集中在Space上面,我们常常会对之前的window做一些隐藏、关闭等操作。

2.隐藏window

这里的隐藏,就是改变原有Window的透明度,把无关的信息隐藏掉,看起来window就“消失”了,其实它并没有关闭。

WindowGroup{
    SolarDisplayView()
        .environment(model)
}
.windowStyle(.plain)

在隐藏之前,一定要设置.windowStyle(.plain),不然会一直显示毛玻璃窗口。

import SwiftUI

// 进入沉浸式空间的入口
struct SolarDisplayView: View {
    @Environment(ViewModel.self) private var model
    
    var body: some View {
        ZStack {
            // 退出沉浸式空间的开关
            SolarSystemToggle()
                .opacity(model.isShowingSolar ? 1 : 0)
            VStack{
                Text("进入沉浸式空间")
                    .font(.system(size: 50, weight: .bold))
                    .padding(.bottom, 15)
                // 进入沉浸式空间的开关
                SolarSystemToggle()
            }
            .opacity(model.isShowingSolar ? 0 : 1)
        }
    }
}

#Preview {
    SolarDisplayView()
        .environment(ViewModel())
}

这里逻辑很简单,就是进入沉浸式空间前,显示进入开关。

进入沉浸式空间后,显示退出开关,其实是同一个window。

这里会有几个疑问:

1. 为什么不直接关闭Window
@Environment(\.dismissWindow) private var dismissWindow
Task {
    await dismissWindow()
}

我们确实可以先关闭window,然后再打开window,但实际使用过程中会出现闪烁的情况,所以才使用隐藏、显示。

2. 退出开关还可以怎么做?

可以直接放在Space空间,但是需要自己调整位置

可以用RealityView.attachment,也需要调整位置

3. 为什么Space、window可以同时存在

我最开始会有这个疑问,为什么能同时存在,做成导航栈一样岂不是更好,一个打开,另一个关闭。直到我看到Destination Video,我才知道它们一起存在的意义。里面展示一个视频播放的demo,最开始是一个视频列表窗口,有导航栈点击进入详情。详情展示了播放窗口,又展示了一个全景的沉浸式空间。在接触这个demo之前,我一直以为播放器也在沉浸式空间里面,但是始终没有达到想要的效果,最终才发现window、space同时存在才能实现。

3.代码

入口是SolarDisplayViewMyWorldApp启动入口也是它,直接Run就可以。