VisionPro开发 - 窗口,空间容器和空间

1,004 阅读3分钟

首页:漫游Apple Vision Pro

Code Repo: github.com/xuchi16/vis…

原文:xz3t11cmy1.feishu.cn/wiki/UaYSw4…


基本概念

visionOS中包含3种基本构建块,窗口(Windows)、空间容器(Volume)和空间(Space)。

window_volume_space_definition

为了理解清楚这3个概念,我们想构建一个应用,包含一个导航页面,其中有3个按钮,用来打开不同的构建块。

image.png

效果

步骤

构建3种View

Window

WindowView是一个普通的SwiftUI View,为了在其中添加3D对象,需要:

  • 将USDZ类型的模型文件添加到项目中

  • 使用RealityKit中的Model3D方法引用模型文件

struct WindowView: View {
    var body: some View {
        Model3D(named: "Sun")
    }
}

在预览中,可以看到一个太阳的模型悬浮在窗口上方

在App中,新增WindowGroup,并给它一个id。

 @main
struct SunApp: App {
    var body: some Scene {
        // ...
  
        WindowGroup(id: "windowView") {
            WindowView()
        }
    }
}

这样,我们就定义好了一个基本的Window。

Volume

Volume也是一个Window,在View的定义上是一样的,唯一的区别是在App中的WindowGroup,需要通过modifier将它的Window style声明为.volumetric

 @main
struct SunApp: App {
    var body: some Scene {
        // ...
        
        WindowGroup(id: "windowView") {
            WindowView()
        }
        
        WindowGroup(id: "volumeView") {
            VolumeView()
        }.windowStyle(.volumetric)
        
        // ...
    }
}

Space

Space的View定义跟Window和Volume有所区别,需要通过RealityView加载场景,并在后续的闭包中加载对象。

struct ImmersiveView: View {
    var body: some View {
        RealityView { content in
            // Add the initial RealityKit content
            if let scene = try? await Entity(named: "Immersive", in: realityKitContentBundle) {
                content.add(scene)
            }
        }
    }
}

在App中,通过ImmersiveSpace添加View。

 @main
struct SunApp: App {
    var body: some Scene {
        // ...
        
        ImmersiveSpace(id: "immersiveView") {
            ImmersiveView()
        }
    }
}

在Reality Composer Pro中,我们可以编辑场景Immersive,导入模型并通过transform修改其位置。当打开Space时,就可以根据设定的场景加载了。

这样,我们就完成了3种场景的构建。

构建导航页

在导航页,我们需要3个按钮,每个按钮可以实现对应View的打开和关闭。想要通过一个按钮实现开关功能,可以使用SwiftUI中的Toggle。

Toggle功能

Toggle Doc

A control that toggles between on and off states.

在使用toggle时,需要定义一个bool变量绑定到isOn,用于决定其状态是开还是关。

@State private var vibrateOnRing = true

var body: some View {
    Toggle(
        "Vibrate on Ring",
        systemImage: "dot.radiowaves.left.and.right",
        isOn: $vibrateOnRing
    )
}

通过按钮打开/关闭Space,首先需要定义Toggle:

  • 新建一个Toggle,通过toggleStyle定义为按钮类型
  • 声明一个bool类型变量isImmersiveSpaceShown,绑定到Toggle控件的isOn上,存储toggle的当前状态
  • 通过onChange注册用户点击按钮会触发的动作,即打开/关闭immersive space

这样就完成了按钮样式和行为的定义

struct ContentView: View {
    
    @State private var isImmersiveSpaceShown = false
    
    var body: some View {
        // ...
        
        Toggle("Space", isOn: $isImmersiveSpaceShown)
            .toggleStyle(.button)
            .onChange(of: isImmersiveSpaceShown) {
                // open/dismiss space
            }
    }
}

为了在用户点击Toggle时打开/关闭Space,需要在View中通过@Environment获取环境中openImmersiveSpace,它的类型是OpenImmersiveSpaceAction

OpenImmersiveSpaceAction定义了callAsFunction(id:)方法,签名如下。因此可以直接调用该方法,通过传递特定的id打开指定的沉浸式空间。该方法为异步方法,根据调用时沉浸空间打开的状态不同,返回的结果也是不同的,这里我们暂时忽略调用的返回结果。

    @discardableResult
    @MainActor public func callAsFunction(id: String) async -> OpenImmersiveSpaceAction.Result

根据上述内容,可以通过如下代码实现对沉浸空间的打开/关闭。

struct ContentView: View {
    
    @State private var isImmersiveSpaceShown = false
    @Environment(.openImmersiveSpace) private var openImmersiveSpace
    @Environment(.dismissImmersiveSpace) private var dismissImmersiveSpace
    
    var body: some View {
        // ...
        
        Toggle("Space", isOn: $isImmersiveSpaceShown)
            .toggleStyle(.button)
            .onChange(of: isImmersiveSpaceShown) { _, show in
                Task {
                    if show {
                        await openImmersiveSpace(id: "immersiveView")
                    } else {
                        await dismissImmersiveSpace()
                    }
                }
            }
    }
}

Window的打开/关闭的实现也是类似的,但openWindowdismissWindow是同步的,所以略有差异。

struct ContentView: View {

    @State private var isVolumeWindowShown = false
    @Environment(.openWindow) private var openWindow
    @Environment(.dismissWindow) private var dismissWindow
    
    var body: some View {
        // ...
        
        Toggle("Volume", isOn: $isVolumeWindowShown)
            .toggleStyle(.button)
            .onChange(of: isVolumeWindowShown) { _, show in
                if show {
                    openWindow(id: "volumeView")
                } else {
                    dismissWindow(id: "volumeView")
                }
            }
    }
}

结果

这样,我们就可以通过页面控制3种类型构建块的加载了

  • 中间:导航页
  • 顶部:Window
  • 左边:Volume
  • 右侧:Space(透视情况下将物体放置在右侧)