ImmersiveSpace 是用来在无限空间中呈现其内容的场景。
该结构体的定义:
swift
struct ImmersiveSpace<Content, Data> where Content : ImmersiveSpaceContent, Data : Decodable, Data : Encodable, Data : Hashable
使用 Immersive Space 作为应用呈现的视图层次结构的容器。 Immersive Space 的内容的层次结构的示例:
swift
@main
struct SolarSystemApp: App {
var body: some Scene {
ImmersiveSpace {
SolarSystem()
}
}
}
ImmersiveSpace 具有以下特性:
- 当你的应用程序打开沉浸式空间时,系统会隐藏所有其他可见的应用程序;
- SwiftUI一次只允许打开一个沉浸式空间。在打开另一个沉浸式空间之前,关闭任何当前打开的沉浸式空间;
- 你可以使用沉浸式空间作为应用程序的主界面。对于有边界的场景,请使用 WindowGroup;对于基于文档的应用程序,请使用 DocumentGroup;
- 你可以选择定义一个 ImmersiveSpace 来呈现符合 Hashable 和 Codable 类型的数据;
- 如果你传递一个数据给 openImmersiveSpace 操作以打开沉浸式空间,那么该空间将把这个数据作为这个空间的根视图的绑定。系统会持久保存这个绑定的值以便用于状态恢复。恢复空间时,系统会解码这个值并用于设定绑定。如果解码失败,系统会将绑定值设定为默认值或空。
创建 ImmersiveSpace
ImmersiveSpace 的初始化方法包含了几种功能的组合,包括:
- 内容类型限制;
- 身份识别;
- 数据传递;
- 数据传递提供默认值。
内容类型限制
ImmersiveSpace 的 init 方法通过 init 增加泛型的形式,提供了针对内容的类型限制:
init(content: () -> Content)
无类型限制
定义:
swift
public init(@ImmersiveSpaceContentBuilder content: @escaping () -> Content) where Data == NoImmersiveSpaceData
因为这个构造方法不需要提供数据的能力所以这里 Data 类型为 NoImmersiveSpaceData,表示没有空间数据。
示例代码:
swift
@main
struct SolarSystemApp: App {
var body: some Scene {
ImmersiveSpace {
ContentView()
}
}
}
注意:要求应用将这个 ImmersiveSpace 作为应用启动的首个 Scene 。
openImmersiveSpace 传递 "" 报错:No Immersive Space with id '' is defined。
init<V>(content: () -> V)
限制内容类型
swift
@main
struct SolarSystemApp: App {
var body: some Scene {
ImmersiveSpace < ImmersiveSpaceViewContent < LimitedClassView >, NoImmersiveSpaceData >() { LimitedClassView . init () }
}
}
开发者无需实现 ImmersiveSpaceViewContent (但 API 依然对外可用),可以通过 init 方法指定泛型:
swift
@main
struct SolarSystemApp: App {
var body: some Scene {
ImmersiveSpace . init < LauncherWithLimitedView > () { LauncherWithLimitedView . init () }
}
}
注意:要求应用将这个 ImmersiveSpace 作为应用启动的首个 Scene 。
openImmersiveSpace 传递 "" 报错:No Immersive Space with id '' is defined。
这一类带有类型限制能力的方法通过拓展来呈现,包括处理数据传递和类型识别的组合:
swift
@available(visionOS 1.0, *)
@available(iOS, unavailable)
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
extension ImmersiveSpace {
public init<V>(@ViewBuilder content: @escaping () -> V) where Content == ImmersiveSpaceViewContent<V>, Data == NoImmersiveSpaceData, V : View
public init<V>(id: String, @ViewBuilder content: () -> V) where Content == ImmersiveSpaceViewContent<V>, Data == NoImmersiveSpaceData, V : View
public init<V>(id: String, for type: Data.Type, @ViewBuilder content: @escaping (Binding<Data?>) -> V) where Content == ImmersiveSpaceViewContent<V>, V : View
public init<V>(for type: Data.Type, @ViewBuilder content: @escaping (Binding<Data?>) -> V) where Content == ImmersiveSpaceViewContent<V>, V : View
public init<V>(id: String, for type: Data.Type = Data.self, @ViewBuilder content: @escaping (Binding<Data>) -> V, defaultValue: @escaping () -> Data) where Content == ImmersiveSpaceViewContent<V>, V : View
public init<V>(for type: Data.Type = Data.self, @ViewBuilder content: @escaping (Binding<Data>) -> V, defaultValue: @escaping () -> Data) where Content == ImmersiveSpaceViewContent<V>, V : View
}
泛型类型的方法后续不再进行说明,可以参考这里查询对应内容。
身份识别
通过增加 id 参数,便于开发者创建 ImmersiveSpace 时,提供一个唯一识别符,后续可以将唯一识别符传递给 openImmersiveSpace ,来打开指定的 Space。
init(id: String, content: () -> Content)
通过 id 参数进行空间识别
方法的定义:
less
public init(id: String, @ImmersiveSpaceContentBuilder content: () -> Content) where Data == NoImmersiveSpaceData
用法示例:
css
@main
struct SolarSystemApp: App {
var body: some Scene {
ImmersiveSpace (id: "ImmersiveSpace" ) { ImmersiveView () }
}
}
init<V(id: String, content: () -> V)
通过 id 增加额外的内容类型限制
与内容类型限制声明相仿,不展开说明。
数据传递
在内容类型限制、身份识别能力基础之上增加传递数据的能力,所以这里会有四种类型:
init(for: Data.Type, content: (Binding<Data?>) -> Content)
无类型限制,无身份识别,提供数据传递能力
定义:
less
///
/// - Parameters:
/// - type: 该空间接受的呈现数据的类型。
/// - content: 一个沉浸式空间内容构建器,用于定义空间的内容。
/// 闭包接收通过 ``EnvironmentValues/openImmersiveSpace`` 传递的值的绑定。
/// 此绑定的值将在空间的状态恢复期间进行持久保存和恢复。
public init(for type: Data.Type, @ImmersiveSpaceContentBuilder content: @escaping (Binding<Data?>) -> Content)
当你使用 EnvironmentValues/openImmersiveSpace
操作呈现指定 type
的值时,会调用此方法。
参数说明:
- type: 该空间接受的呈现数据的类型。
- content: 一个沉浸式空间内容构建器,用于定义空间的内容。
使用方式:
swift
// 1. 声明 Space
@main
struct SolarSystemApp: App {
var body: some Scene {
ImmersiveSpace . init (for: String . self ) { binding in PresentParamDataView (data: binding.wrappedValue ?? "default" ) }
}
}
// 2. 打开该 Space
func openSpaceWithData ( data : String ) { Task { await openImmersiveSpace(value: data) } }
openSpaceWithData 方法将 data 数据,通过 openImmersiveSpace 传递给了对应声明的空间。
这种方式没有身份识别,但是可以通过传递的数据类型进行空间匹配。
如果同时配置两个接收相同类型的 Space,则系统会同时打开,打开空间的顺序遵循 App 中配置 Space 的顺序,但因为一次只能打开一个空间,所以成功打开第一个,后面的都无法打开,并报错:
vbnet
Unable to present another Immersive Space when one is already requested or connected
init<V(for: Data.Type, content: (Binding<Data?>) -> V)
同类型 1。
init(id: String, for: Data.Type, content: (Binding<Data?>) -> Content)
swift
public init(id: String, for type: Data.Type, @ImmersiveSpaceContentBuilder content: @escaping (Binding<Data?>) -> Content)
相较于类型 1,增加了 id 参数,也就是身份验证的能力。
参数说明:
- id: 用于唯一标识沉浸式空间的字符串。
- type: 该空间接受的呈现数据的类型。
- content: 一个沉浸式空间内容构建器,用于为空间的每个实例创建内容。
因为增加了 id 作为空间唯一识别符,所以使用 openImmersiveSpace 传递给了对应声明的空间。
用法示例:
swift
// 1. 声明 Space
@main
struct SolarSystemApp: App {
var body: some Scene {
ImmersiveSpace . init (id: "Space1" , for: String . self ) { binding in PresentParamDataView (data: binding.wrappedValue ?? "default" ) }
}
}
// 2. 打开该 Space
func openSpaceWithData ( data : String ) { Task { await openImmersiveSpace(id: "Space1" , value: data) } }
这样就可以打开指定 id 对应的空间。
init<V(id: String, for: Data.Type, content: (Binding<Data?>) -> V)
同类型 3。
数据传递默认值
这一部分的方法相较于数据传递部分只是增加了默认值能力的拓展。
init(for: Data.Type, content: (Binding<Data) -> Content, defaultValue: () -> Data)
定义:
swift
public init(id: String, for type: Data.Type = Data.self, @ImmersiveSpaceContentBuilder content: @escaping (Binding<Data>) -> Content, defaultValue: @escaping () -> Data)
init<V(for: Data.Type, content: (Binding<Data) -> V, defaultValue: () -> Data)
init<V(id: String, for: Data.Type, content: (Binding<Data) -> V, defaultValue: () -> Data)
init(id: String, for: Data.Type, content: (Binding<Data) -> Content, defaultValue: () -> Data)
声明示例:
swift
ImmersiveSpace . init (id: "Space2" , for: String . self ) { binding in ContentView (name: binding.wrappedValue) } defaultValue: { "A" }
需要注意的是这里 defaultValue 通过 closure 的形式来提供;另外一点是相较于没有提供默认值的方法,binding 的
wrappedValue 变为非空,而不需要处理可能为 nil 的情况。
打开 ImmersiveSpace
SwiftUI 的 EnvironmentValues 中提供了 openImmersiveSpace 用于展示沉浸式空间的操作的拓展。
swift
extension EnvironmentValues {
public var openImmersiveSpace: OpenImmersiveSpaceAction { get }
}
调用会在沉浸式空间呈现后或发生错误时返回。该方法适用于 SwiftUI。
下面是个使用示例,展示了定义一个按钮,用于在新空间中打开指定的太阳系空间:
swift
@main
struct SolarSystemApp : App {
var body: some Scene {
ImmersiveSpace (for: SolarSystem . ID . self ) { $solarSystemID in
// ...
}
}
}
struct NewSolarSystemImmersiveSpace : View {
var solarSystem: SolarSystem
@Environment (.openImmersiveSpace) private var openImmersiveSpace
var body: some View {
Button ( "在新沉浸式空间中展示太阳系" ) {
Task {
await openImmersiveSpace(value: solarSystem. ID )
}
}
}
}
为了获得最佳性能,value 应使用轻量级数据。对于符合可识别的结构化 Model 的值,值的标识符是一个很好的表示值。
openImmersiveSpace 属性类型是 OpenImmersiveSpaceAction,只是实现了 callAsFunction ,所以能够作为函数调用。
Swift 5.2的新功能是能够将类型的实例作为函数来调用。 或者,如Swift Evolution提案所称,它是"用户定义的标称类型的可调用值"。 此功能的简短描述是,它允许您调用实现了callAsFunction方法的任何类型的实例,就好像它是一个函数一样。docs.swift.org/swift-book/...
less
public struct OpenImmersiveSpaceAction : Sendable {
@discardableResult
@MainActor public func callAsFunction(id: String) async -> OpenImmersiveSpaceAction.Result
@discardableResult
@MainActor public func callAsFunction<D>(value: D) async -> OpenImmersiveSpaceAction.Result where D : Decodable, D : Encodable, D : Hashable
@discardableResult
public func callAsFunction<D>(id: String, value: D) async -> OpenImmersiveSpaceAction.Result where D : Decodable, D : Encodable, D : Hashable
}
callAsFunction 提供了三种类型分别对应:
callAsFunction(id: String)
, 仅通过 id 打开 SpacecallAsFunction<D>(value: D)
,仅通过数据打开 SpacecallAsFunction<D>(id: String, value: D)
, 通过 id 打开 Space,并传递数据。
需要注意的是这些调用都被设计成异步的操作。 返回类型是 OpenImmersiveSpaceAction.Result
:
swift
public struct OpenImmersiveSpaceAction : Sendable {
/// Represents the result of opening an immersive space.
public enum Result : Sendable {
/// Opening the immersive space succeeded.
case opened
/// Opening the immersive space failed since the user cancelled the
/// request.
case userCancelled
/// Opening the immersive space failed since the system cannot fulfill
/// the request.
case error
public static func == (a: OpenImmersiveSpaceAction.Result, b: OpenImmersiveSpaceAction.Result) -> Bool
public func hash(into hasher: inout Hasher)
public var hashValue: Int { get }
}
}
枚举类型表示打开空间的结果,有三种类型:
- 成功打开沉浸式空间
- 由于用户取消,打开沉浸式空间失败
- 由于系统无法满足请求,打开沉浸式空间失败
关闭 ImmersiveSpace
SwiftUI 的 EnvironmentValues 中提供了 dismissImmersiveSpace 用于关闭沉浸式空间的操作的拓展。
swift
extension EnvironmentValues {
public var dismissImmersiveSpace: DismissImmersiveSpaceAction { get }
}
调用会在沉浸式空间关闭后返回。该方法适用于 SwiftUI。
用法示例:
swift
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
ImmersiveSpace(id: "solarSystem") {
SolarSystemView()
}
}
}
struct DismissImmersiveSpaceButton: View {
@Environment(.dismissImmersiveSpace) private var dismissImmersiveSpace
var body: some View {
Button("关闭太阳系沉浸式空间") {
Task {
await dismissImmersiveSpace()
print("关闭完成")
}
}
}
}
dismissImmersiveSpace 的类型是 DismissImmersiveSpaceAction,也是通过 callAsFunction 提供的函数调用形式的能力:
swift
public struct DismissImmersiveSpaceAction {
/// 关闭当前打开的沉浸式空间。
/// 调用会在空间关闭后返回。
///
/// 不要直接调用此方法。SwiftUI 在你调用 ``EnvironmentValues/dismissImmersiveSpace`` 操作时会调用它:
/// await dismissImmersiveSpace()
@MainActor public func callAsFunction () async
}
注意这也是一个异步的方法。
空间类型声明
在声明 ImmersiveSpace 时,通过 immersionStyle 指定空间的类型:
swift
ImmersiveSpace (id: "ImmersiveSpace" ) {
TestForParamSpace ()
}
.immersionStyle(selection: $immersionState , in: .progressive)
immersionStyle 的定义:
swift
@available (visionOS 1.0 , * )
@available ( iOS , unavailable)
@available ( macOS , unavailable)
@available ( tvOS , unavailable)
@available ( watchOS , unavailable)
extension Scene {
/// Sets the allowed styles for the immersive space.
/// - Parameters:
/// - selection: A binding to the effective style used by the space.
/// - styles: The list of styles that the immersive space allows.
public func immersionStyle ( selection : Binding < ImmersionStyle >, in styles : ImmersionStyle ...) -> some Scene
}
作为 Scene 的拓展的形式提供的,而不是 ImmersiveSpace 的拓展 🤔。意思是 WindowGroup 也能用?
它有两个参数:
- selection:绑定到空间使用的有效样式。
- styles:沉浸式空间允许的风格列表。
需要注意的是,selection 如果不在 styles 允许的范围内,默认会使用 styles 列表中的第一个 style 风格。
ImmersionStyle
ImmersionStyle 代表沉浸式空间的风格。
风格影响沉浸式空间的外观和行为。要配置沉浸式空间的当前风格,请使用immersionStyle(selection:in:)
修饰符。在创建空间时指定符合 ImmersionStyle 的样式。
内置样式类型包括四种:
static var automatic: AutomaticImmersionStyle
默认的沉浸式风格。默认情况下,SwiftUI 将 ImmersionStyle/mixed
样式作为自动样式。
static var full: FullImmersionStyle
一种沉浸式样式,显示 pass-through ****视频的无界限内容。空间内容完全遮挡了透视视频,除了用户的手配置为显示的情况下,手部可以穿透展示。
static var mixed: MixedImmersionStyle
一种沉浸式样式,显示与其他应用程序内容混合的无界限内容,并显示透视视频。
static var progressive: ProgressiveImmersionStyle
一种沉浸式样式,其中内容显示时没有应用剪裁边界。
系统最初使用 Portal 效果。随后,人们可以交互式地调整缩放,以在 Portal 样式和与FullImmersionStyle
匹配的样式之间切换。在后一种情况下,如果根据配置,透视效果会被遮挡,除了用户的手。