WWDC Quick Look 💓 By SwiftGGTeam
Take your iPad apps to the next level

Take your iPad apps to the next level

观看原视频

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.keyCommandsaddKeyCommand 声明的快捷键会出现在一个未分类的区域。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.ActivationActionUIAction 的子类,可以直接用在菜单、按钮和导航栏按钮中
  • 闭包返回 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.stateselectionRect 更新选择状态。结合 initialModifierFlags 支持 Shift 追加选择和 Command 切换选择。

4. 用 Pointer Accessories 提示可拖拽视图

当指针悬停在可拖拽视图上时,显示箭头配件提示拖拽方向。这比单纯的指针形状变化更直观。

实现思路:在 UIPointerInteractionstyleFor 方法中,根据视图状态返回带 accessoriesUIPointerStyle。结合 latchingAxes 让指针效果在拖拽时跟随视图。

5. 完善场景状态恢复

用户切换 App 或重启设备后,期望回到之前的工作状态。保存内容状态和交互状态(光标位置、滚动位置)。

实现思路:在 stateRestorationActivity(for:) 中保存内容和 interactionState。在 scene(_:restoreInteractionState:) 中恢复。如果恢复需要异步加载,使用 extendStateRestoration()completeStateRestoration()

关联 Session

评论

GitHub Issues · utterances