构建桌面级 iPad 应用
Build a desktop-class iPad app
2022年6月6日
一句话判断
iPadOS 16 终于给了 iPad 一个”正经电脑”该有的 UI 密度——导航栏编辑器模式、可自定义工具栏、多选菜单,这套 API 的完成度让人觉得 iPad 距离取代笔记本又近了一步。
这场 Session 讲了什么
这场 Session 以一个 Markdown 编辑器为例,完整展示了怎么把一个 iPadOS 15 的 App 升级到桌面级体验。核心改动围绕三块展开:导航栏、多选交互、文本编辑。
导航栏方面,iPadOS 16 引入了三种导航样式:Navigator(传统的 push/pop)、Browser(适合文件浏览器)和 Editor(适合文档编辑器)。选定 Editor 样式后,标题会靠左对齐,中间腾出空间放工具按钮。这个设计思路和 macOS 应用的工具栏几乎一模一样。更妙的是,这些按钮是可自定义的——用户可以通过拖拽来增删工具栏项,就像在 Mac 上自定义工具栏一样。
多选交互部分,UICollectionView 现在支持轻量级多选(不用进入编辑模式),配合新的多选菜单 API,选中多个项目后右键可以直接弹出批量操作菜单。文本编辑方面,UITextView 加了 isFindInteractionEnabled 属性,一行代码开启系统级查找替换;新的 Edit Menu API 让你可以往编辑菜单里塞自定义操作。
整个过程里,Apple 还反复提醒一件事:你在 iPad 上做的这些改动,在 Mac Catalyst 上会自动转换成对应的 macOS 交互——工具栏变 macOS toolbar,右键菜单变原生菜单,系统操作会自动出现在 File 菜单里。
值得深挖的点
Per-primitive data:这不是小优化,是架构层面的改变。 之前的 alpha testing 交叉函数需要经过四层指针间接引用才能拿到纹理数据——先读 instance data,再读 material index,再读 material,最后采样纹理。现在直接把纹理和 UV 坐标塞进 acceleration structure 的 per-primitive data 里,交叉函数只需要一次内存读取。Apple 自己的测试应用显示性能提升 10%-16%。我的判断是,对于复杂场景,这个优化会更明显,因为缓存命中率的改善会随场景复杂度放大。
Heap 分配加速结构:解决大规模场景的 useResource 瓶颈。 之前每帧渲染时,instance acceleration structure 需要为每个 primitive acceleration structure 调用 useResource:,场景大的时候可能要调几千次。把所有 primitive acceleration structure 分配到同一个 heap 之后,只需要调一次 useHeap: 就能让所有资源常驻。这个改动在 API 层面很小,但对大场景的性能影响可能很显著。
代码片段
设置 Editor 导航样式并配置 title menu:
// 设置编辑器导航样式
navigationItem.style = .editor
// 用 UIDocumentProperties 配置文档信息
let docProperties = UIDocumentProperties(url: document.fileURL)
let itemProvider = NSItemProvider(object: document.fileURL as NSURL)
docProperties.dragItemsProvider = { _ in
[UIDragItem(itemProvider: itemProvider)]
}
docProperties.activityViewControllerProvider = {
UIActivityViewController(activityItems: [document.fileURL],
applicationActivities: nil)
}
navigationItem.documentProperties = docProperties
注意 documentProperties 会自动在 title menu 顶部生成文档头信息,包括文件名、大小和图标。拖拽图标可以直接复制文件到其他 App。
可自定义的中心区域工具栏:
// 固定组——用户无法移除
let syncGroup = UIBarButtonItem.creatingFixedGroup(
customizableView: syncButton, // 同步滚动按钮
representation: nil
)
// 可选组——默认不在工具栏,但用户可以添加
let formatGroup = UIBarButtonItemGroup.optionalGroup(
customizableView: formatButton,
representativeItem: UIBarButtonItem(title: "Format"),
isInDefaultCustomization: false
)
navigationItem.centerItemGroups = [syncGroup, ..., formatGroup]
navigationItem.customizationIdentifier = "com.example.editor.toolbar"
customizationIdentifier 是关键——设了这个属性,用户对工具栏的自定义才会持久化。
查找替换和自定义编辑菜单:
// 一行代码开启查找替换
textView.isFindInteractionEnabled = true
// 自定义编辑菜单
func editMenu(forTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu {
let hideAction = UIAction(title: "Hide") { _ in
// 隐藏选中文本
}
return UIMenu(children: [hideAction] + suggestedActions)
}
最佳实践
选择导航样式时要根据内容类型决定,不要无脑用 Editor——层级数据用 Navigator,文件浏览用 Browser,单文档编辑才用 Editor。
title menu 里优先使用系统提供的操作(rename、duplicate、move),它们自带本地化标题和图标,还会自动出现在 Mac Catalyst 的 File 菜单里。自定义操作需要手动添加到 main UIMenuSystem 才能在 Mac 上显示。
UICollectionView 多选时,把 selectionFollowsFocus 设为 true 让键盘导航驱动选择,同时用新的 performPrimaryActionForItemAtIndexPath 替代 didSelectItemAtIndexPath 来处理单击行为——这样多选时就不会误触发跳转了。
还有什么值得关注
- 导航栏的 overflow menu 会自动把放不下的 UIBarButtonItem 转成菜单项,但自定义 view 的 UIBarButtonItem 需要手动设
menuRepresentation。 preferredElementSize属性可以让菜单项变小尺寸的内联样式,配合keepsMenuPresented可以实现不关闭菜单的连续操作(比如字号调节)。- Mac Catalyst 下,Editor 样式的导航栏会被翻译成标准的 macOS 窗口标题栏,中心区域变成可自定义的 macOS toolbar。