Highlight
iPadOS 15 引入了三项提升 iPad App 生产力的核心能力:Prominent Scenes 实现模态化的专注交互,UIMenuBuilder 让键盘快捷键拥有分类可搜索的菜单界面,Pointer Accessories 和 Band Selection 为指针操作增加了多选和视觉提示能力。
核心内容
iPad 用户越来越依赖这台设备完成复杂工作。但多窗口管理、键盘快捷键发现、指针交互这些桌面级能力,在 iPad 上一直不够完善。iPadOS 15 从这三个方向同时发力。
Prominent Scenes:专注模式的新窗口
(02:54)
iPadOS 13 引入了多窗口,但所有窗口都是平级的。iPadOS 15 新增了 prominent 展示样式,让新窗口以模态方式呈现,背景窗口自动变暗。这适合需要专注处理单个文档或邮件的场景。
使用 prominent 的场景应该满足三个条件:内容独立完整、提供 Done 或 Close 按钮、专注于特定内容(如单个文档)。系统提供了 UIWindowScene.ActivationAction,让开发者只需几行代码就能在上下文菜单中添加”在新窗口中打开”选项。
键盘快捷键菜单:从隐藏到可发现
(14:11)
iPadOS 15 长按 Command 键会弹出全新的快捷键菜单。菜单按 File、Edit、View 等分类组织,支持搜索,还能直接点击触发。这和 Mac Catalyst 的菜单栏共享同一套 UIMenuBuilder API。
关键变化:以前通过 UIResponder.keyCommands 或 addKeyCommand 声明的快捷键会出现在一个未分类的区域。iPadOS 15 要求所有快捷键都通过 buildMenu(with:) 注册到主菜单系统中,才能获得完整的分类和搜索体验。
指针增强:Band Selection 和 Accessories
(26:42)
iPadOS 15 从 Mac 借来了 Band Selection(框选)交互。在支持多选的 UICollectionView 上,点击拖拽会自动拉出选择矩形,框住的单元格被选中。Shift 键追加选择,Command 键切换选择状态。
Pointer Accessories 是另一项新能力。开发者可以在指针上附加箭头、加号等视觉提示,告诉用户某个视图可以拖拽或执行特定操作。Accessories 独立于指针样式,可以和 lift、highlight 等效果组合使用。
详细内容
创建”在新窗口中打开”操作
(04:56)
let newSceneAction = UIWindowScene.ActivationAction { _ in
let userActivity = NSUserActivity(activityType: "com.myapp.detailscene")
return UIWindowScene.ActivationConfiguration(userActivity: userActivity)
}
关键点:
UIWindowScene.ActivationAction是UIAction的子类,可以直接用在菜单、按钮和导航栏按钮中- 闭包返回
UIWindowScene.ActivationConfiguration,其中包含一个NSUserActivity表示新场景的内容 - 在 iPad 和 Mac Catalyst 上显示”在新窗口中打开”,在 iPhone 上自动隐藏
如果需要为 iPhone 提供替代行为,可以传入 alternate 参数:
let alternateAction = UIAction(title: "Show Details", image: nil) { _ in
// iPhone 上的替代行为
}
let newSceneAction = UIWindowScene.ActivationAction(
alternate: alternateAction
) { _ in
let userActivity = NSUserActivity(activityType: "com.myapp.detailscene")
return UIWindowScene.ActivationConfiguration(userActivity: userActivity)
}
Collection View 手势打开新场景
(06:58)
func collectionView(_ collectionView: UICollectionView,
sceneActivationConfigurationForItemAt indexPath: IndexPath,
point: CGPoint) -> UIWindowScene.ActivationConfiguration? {
guard let itemActivity = itemUserActivity(at: indexPath) else {
return nil
}
return UIWindowScene.ActivationConfiguration(userActivity: itemActivity)
}
关键点:
- 实现
UICollectionViewDelegate的新方法,支持双指捏合展开手势 - 返回
nil表示该单元格不支持在新窗口中打开 - 只能用于
prominent样式的场景展示
自定义场景转场预览
(08:53)
let itemActivity = NSUserActivity(activityType: "com.myapp.detailscene")
let configuration = UIWindowScene.ActivationConfiguration(userActivity: itemActivity)
if let cell = collectionView.cellForItem(at: indexPath) as? ThumbnailCell {
configuration.preview = UITargetedPreview(view: cell.thumbnailView)
}
return configuration
关键点:
- 默认转场从整个单元格开始,通过设置
preview可以让转场从单元格的某个子视图开始 UITargetedPreview指定了转场动画的起点视图- 这让动画更精确地反映内容关系
场景状态保存与恢复
(10:18)
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
guard let viewController = self.window?.rootViewController as? EditorViewController else {
return nil
}
let stateActivity = NSUserActivity(activityType: "com.myapp.editorstate")
stateActivity.addUserInfoEntries(from: [
"content": viewController.textField.text ?? "",
"interactionState": viewController.textField.interactionState ?? NSNull()
])
return stateActivity
}
关键点:
interactionState是 iPadOS 15 的新属性,包含了光标位置、滚动位置、第一响应者状态等交互信息- 必须先恢复内容,再恢复交互状态
- iPadOS 15 新增了
scene(_:restoreInteractionState:)委托方法,在 storyboard 加载完成后、首次进入前台之前调用
异步恢复状态:
func scene(_ scene: UIScene, restoreInteractionState stateRestorationActivity: NSUserActivity) {
guard let viewController = window?.rootViewController as? EditorViewController else { return }
scene.extendStateRestoration()
loadContentAsync { result in
viewController.textField.text = result.content
scene.completeStateRestoration()
}
}
关键点:
extendStateRestoration()请求延长恢复时间,启动图会保持显示- 完成恢复后必须调用
completeStateRestoration(),否则系统会强制关闭 App - 不适用于网络请求等可能长时间运行的任务
自定义主菜单
(17:15)
override func buildMenu(with builder: UIMenuBuilder) {
super.buildMenu(with: builder)
guard builder.system == .main else { return }
let tabMenu = UIMenu(options: .displayInline, children: [
UIKeyCommand(title: "New Tab",
action: #selector(newTab(_:)),
input: "t",
modifierFlags: .command),
UIKeyCommand(title: "Close Tab",
action: #selector(closeTab(_:)),
input: "w",
modifierFlags: .command)
])
builder.insertChild(tabMenu, atStartOfMenu: .file)
let bookmarksMenu = UIMenu(title: "Bookmarks", children: [...])
builder.insertSibling(bookmarksMenu, afterMenu: .view)
}
关键点:
- 在
UIApplicationDelegate中重写buildMenu(with:) builder.system == .main确保只在修改主菜单时执行UIMenu.Identifier提供了系统菜单的标识符常量- 每个菜单元素必须有唯一标识符,重复的快捷键组合会导致插入失败并输出控制台日志
动态控制快捷键可用性
(22:38)
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(closeTab(_:)) {
return !openTabs.isEmpty
}
return super.canPerformAction(action, withSender: sender)
}
override func validate(_ command: UICommand) {
if command.action == #selector(toggleBookmark(_:)) {
command.title = currentTab.isInBookmarks
? "Remove from Bookmarks"
: "Add to Bookmarks"
} else {
super.validate(command)
}
}
关键点:
canPerformAction决定快捷键是否可执行,返回false时快捷键在菜单中隐藏validate在快捷键被触发前调用,可以动态修改标题、图标等外观- 两个方法都必须调用
super处理未覆盖的情况
Band Selection 多选交互
(28:47)
let selectionInteraction = UIBandSelectionInteraction { [weak self] interaction in
guard let self = self else { return }
if interaction.state == .selecting {
self.selectItemsInRect(interaction.selectionRect)
} else if interaction.state == .ended {
self.finalizeSelection()
}
}
view.addInteraction(selectionInteraction)
关键点:
UIBandSelectionInteraction适用于自定义视图,UICollectionView 自动支持selectionRect是当前选择框的矩形区域initialModifierFlags提供拖拽开始时按下的修饰键(Shift、Command 等)
Pointer Accessories
(33:27)
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
let preview = UITargetedPreview(view: self)
let style = UIPointerStyle(effect: .lift(preview))
if #available(iOS 15.0, *) {
style.accessories = [
.arrow(.left),
.arrow(.right)
]
}
return style
}
关键点:
accessories是 iOS 15 新增的UIPointerStyle属性.arrow(.left)和.arrow(.right)是系统预定义的箭头配件- 配件位置和角度可以自定义:
UIPointerAccessory.Position(offset: 23.0, angle: .pi * 1.25)
核心启发
1. 为文档类 App 添加 Prominent Scene 支持
如果你的 App 处理文档、邮件、笔记等内容,让用户可以在新窗口中打开单个条目。这直接提升多任务效率。
实现思路:为每个内容条目创建 NSUserActivity,在上下文菜单中加入 UIWindowScene.ActivationAction。设置 activationConditions 确保系统正确路由请求。
2. 整理键盘快捷键到主菜单系统
审计现有代码中所有 keyCommands 属性和 addKeyCommand 调用,迁移到 buildMenu(with:)。这让快捷键在 iPad 和 Mac 上都有统一的发现体验。
实现思路:按功能分类(File、Edit、View 等),为每个命令设置合适的 discoverabilityTitle,处理快捷键冲突(如 Command-I 与斜体快捷键冲突时改用 Control-Command-I)。
3. 在自定义视图上实现 Band Selection
如果你的 App 有类似画布、网格的布局,用户可能需要框选多个项目。UIBandSelectionInteraction 让这变得简单。
实现思路:创建 UIBandSelectionInteraction,根据 interaction.state 和 selectionRect 更新选择状态。结合 initialModifierFlags 支持 Shift 追加选择和 Command 切换选择。
4. 用 Pointer Accessories 提示可拖拽视图
当指针悬停在可拖拽视图上时,显示箭头配件提示拖拽方向。这比单纯的指针形状变化更直观。
实现思路:在 UIPointerInteraction 的 styleFor 方法中,根据视图状态返回带 accessories 的 UIPointerStyle。结合 latchingAxes 让指针效果在拖拽时跟随视图。
5. 完善场景状态恢复
用户切换 App 或重启设备后,期望回到之前的工作状态。保存内容状态和交互状态(光标位置、滚动位置)。
实现思路:在 stateRestorationActivity(for:) 中保存内容和 interactionState。在 scene(_:restoreInteractionState:) 中恢复。如果恢复需要异步加载,使用 extendStateRestoration() 和 completeStateRestoration()。
关联 Session
- What’s new in UIKit — UIKit 框架在 iOS 15 中的全面更新,包括按钮、列表、SF Symbols 等
- Focus on iPad keyboard navigation — iPad 键盘导航的 Focus 系统详解
- Meet the UIKit Button System — iOS 15 全新的 UIButton.Configuration API
- Build for the iPadOS pointer — iPadOS 指针交互 API 的深入讲解
评论
GitHub Issues · utterances