visionOS 中 Quick Look 的新功能
What’s new in Quick Look for visionOS
2024年6月10日
一句话判断
visionOS 2 的 Quick Look 拿到了全新的 PreviewApplication API,几行代码就能在你的 app 里嵌入窗口化的空间媒体预览,加上 3D 模型的 Surface Mapping 和 Configurations 支持,这是做 visionOS 内容消费类应用必看的一场。
这场 Session 讲了什么
Quick Look 是 Apple 全平台通用的文件预览和编辑工具,在 visionOS 上因为 3D 内容的存在有了独特的价值。visionOS 上预览 3D 内容有两种模式:内嵌式(作为全屏 cover 嵌入你的 app)和窗口式(在独立的 volume 中展示,支持多任务)。今年 Apple 推出了全新的 PreviewApplication API,基于 SwiftUI 和 Swift Concurrency 构建,目标是让你用最少代码实现最大控制。
这个 API 做了几件关键的事:可以预览单个文件,也可以打开一组文件的集合视图;可以自定义默认的编辑选项(比如关闭裁剪功能);提供了直观的方式来管理预览文件的生命周期。Demo 中展示了一个旅行分享 app,点击缩略图就能在窗口化的 Quick Look 中播放空间视频,同时还能继续操作 app 本身。
在 3D 内容方面,两个呼声最高的功能终于来了。Surface Mapping 让任何 3D 模型都可以吸附到桌面或地板等水平面上,拖动窗口栏靠近桌面就能触发吸附,而且吸附后可以自由在桌面上滑动模型。Configurations 则允许在一个 USDZ 文件中包含多个变体(比如不同颜色的 iPhone),用户可以在 Quick Look 中直接切换,不用为每个颜色单独准备文件。
值得深挖的点
PreviewApplication API 的会话生命周期管理
这个 API 最精妙的设计是会话(Session)概念。当你调用 PreviewApplication.open 时,返回值是一个 session 实例,它的 events 属性是一个 Swift AsyncSequence。你可以监听这个流来获知预览窗口何时打开、何时关闭。
这意味着什么?你可以在 UI 上精确地反映预览状态——比如用一个小眼睛图标表示某个文件正在 Quick Look 中预览,关闭后图标自动消失。更关键的是,API 保证了同一个文件只会有一个预览实例。重复打开同一文件不会创建新窗口,而是把已有的预览带到前台。这个去重逻辑帮你省去了手动管理窗口状态的麻烦。
从架构角度看,这是一个很好的 Swift Concurrency 应用案例:用 AsyncSequence 替代传统的 delegate 回调,代码更清晰,也更容易和现有的 async/await 流程组合使用。
Surface Mapping 的设计细节
Surface Mapping 的实现比看上去复杂得多。它自动为所有 3D 模型启用,无需额外代码。但背后有几个值得注意的约束:模型吸附到平面后,pitch 旋转会被禁用,防止模型穿透桌面。为了让吸附效果自然,模型的底部需要放在原点位置——如果你的模型导出时底部偏离了原点,吸附后就会出现”悬浮”或”陷入桌面”的问题。
这其实反映了空间计算中一个反复出现的主题:物理世界的规则会反过来约束你的数字内容。在传统屏幕上,模型的坐标原点无关紧要;但在空间计算中,这个原点决定了你的模型和真实世界的关系。
代码片段
使用 PreviewApplication 打开空间视频预览
import QuickLook
// 打开单个文件的窗口化预览
struct ThumbnailImage: View {
let fileURL: URL
@State private var session: PreviewApplication.Session?
@State private var isPreviewOpen = false
var body: some View {
Image(systemName: "photo")
.onTapGesture {
// 使用 PreviewApplication 打开窗口化 Quick Look
Task {
session = await PreviewApplication.open(url: fileURL)
observeSession()
}
}
// 根据预览状态显示指示器
.overlay {
if isPreviewOpen {
Image(systemName: "eye")
.opacity(0.6)
}
}
}
// 监听预览的打开和关闭事件
private func observeSession() {
guard let session else { return }
Task {
for await event in session.events {
switch event {
case .opened:
isPreviewOpen = true
case .closed:
isPreviewOpen = false
}
}
}
}
}
场景:在旅行 app 中点击缩略图预览空间视频。坑点:session 必须被强引用持有,如果被释放了事件流就会中断。
自定义预览项的显示名称和编辑模式
// 使用 PreviewItem 自定义标题和禁用编辑功能
let previewItem = PreviewItem(
url: selectedURL,
displayName: "Galapagos Trip", // 顶部菜单显示的标题
editingMode: .disabled // 禁用裁剪等编辑选项
)
Task {
session = await PreviewApplication.open(previewItem: previewItem)
}
场景:你希望控制用户在 Quick Look 中能做什么,比如只允许查看不允许编辑。坑点:editingMode 除了 .disabled 还有其他选项,需要查阅文档确认哪种模式适合你的场景。
打开文件集合视图
// 打开一组文件,聚焦到选中的那个
let allURLs = entry.files.map { $0.url } // 当前条目的所有文件
Task {
session = await PreviewApplication.open(
urls: allURLs,
selectedURL: selectedFile.url // 集合视图首先展示这个文件
)
}
场景:旅行 app 中一次预览某个条目下的所有空间视频和照片。坑点:集合视图的导航箭头体验很好,但文件数量太多时需要考虑加载性能。
最佳实践
- 优先使用窗口化预览:在 visionOS 上,窗口化 Quick Look 让用户可以同时操作你的 app 和预览内容,内嵌式预览会打断工作流。
- 3D 模型底部对齐原点:为了让 Surface Mapping 吸附效果正确,确保导出的 USDZ 模型底部落在坐标原点上。
- 合理设置编辑模式:如果你的 app 不需要用户在 Quick Look 中编辑文件,主动设置
editingMode: .disabled,避免用户误操作修改原始文件。 - 利用 session 事件管理 UI 状态:通过监听 session 的打开/关闭事件,在 UI 上同步反映预览状态,让用户清楚知道哪些文件正在查看中。
- 空间媒体专项参考:配合《Building compelling spatial photo and video experiences》Session 一起看,了解如何为 app 创建空间照片和视频内容。
还有什么值得关注
- Configurations 功能让一个 USDZ 文件包含多个变体成为可能,对于电商类应用展示不同颜色/款式的产品非常有用。
- Quick Look 的编辑结果默认会回写到原始文件,如果你的 app 有自己的文件管理逻辑,需要注意这个行为。
- 通过 drag and drop 获取窗口化 Quick Look 的旧方式仍然可用,但新的 PreviewApplication API 提供了更精细的控制能力。