让你的 iPad app 更上一层楼
Take your iPad apps to the next level
2021年6月9日
一句话判断
iPad app 在 iPadOS 15 获得了一大波生产力增强——多任务分屏、键盘快捷键、全系统拖拽支持——如果你的 iPad app 还只是一个放大版 iPhone app,这场 Session 给你列出了升级清单。
这场 Session 讲了什么
iPad 作为生产力设备,用户对 app 的期望远高于 iPhone。这场 Session 聚焦于让 iPad app 充分利用平台的独特能力,涵盖多任务、键盘交互、拖拽和 SwiftUI 新特性。
多任务方面,iPadOS 15 的 Shelf(多窗口管理器)让用户可以同时打开同一个 app 的多个窗口。你的 app 需要支持 UIScene 的多实例模式,每个窗口展示不同的文档或内容。
键盘方面,iPad 上的外接键盘体验在 iPadOS 15 进一步增强。全局快捷键(Cmd+N 新建、Cmd+W 关闭窗口)已经成为标配。UIKeyCommand 现在可以直接绑定到 UIBarButtonItem,不需要额外的 target-action。
拖拽方面,iPadOS 15 强化了跨 app 拖拽体验。用户可以从你的 app 拖拽内容到另一个 app 中。你需要实现 UIDragItem 的 NSItemProvider 来提供标准化的数据格式(文本、图片、URL)。
SwiftUI 方面,新的 keyboardShortcut 修饰符让快捷键绑定变得极其简单。
值得深挖的点
多窗口(Multi-window)的状态隔离
当用户通过 Shelf 打开多个窗口时,每个窗口对应一个独立的 UIScene。你的 app 需要确保不同窗口之间的状态完全隔离——一个窗口的数据修改不应该影响另一个窗口。如果使用 Core Data,每个 scene 应该有自己的 NSManagedObjectContext,而不是共享一个全局 context。
拖拽的数据格式协商
当你拖拽一个项目到另一个 app 时,NSItemProvider 支持注册多种数据表示(representation)。比如拖拽一张图片时,可以同时提供 JPEG、PNG 和 URL 三种格式。接收方会选择它能处理的最佳格式。Session 建议至少提供两种表示:一种富格式(比如图片的二进制数据)和一种通用格式(比如 URL 字符串)。
代码片段
// 支持多窗口的 Scene 配置
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var document: Document?
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
// 每个窗口使用独立的 document 实例
let document = Document()
self.document = document
let viewController = DocumentViewController(document: document)
window = UIWindow(windowScene: windowScene)
window?.rootViewController = viewController
window?.makeKeyAndVisible()
}
}
// 在 Info.plist 中声明支持多窗口
// UIApplicationSceneManifest -> UIApplicationSupportsMultipleScenes = YES
// SwiftUI 的键盘快捷键
import SwiftUI
struct DocumentToolbar: ToolbarContent {
let onNew: () -> Void
let onSave: () -> Void
let onUndo: () -> Void
var body: some ToolbarContent {
ToolbarItemGroup(placement: .primaryAction) {
Button(action: onNew) {
Label("新建", systemImage: "doc.badge.plus")
}
.keyboardShortcut("n", modifiers: .command)
Button(action: onSave) {
Label("保存", systemImage: "square.and.arrow.down")
}
.keyboardShortcut("s", modifiers: .command)
Button(action: onUndo) {
Label("撤销", systemImage: "arrow.uturn.backward")
}
.keyboardShortcut("z", modifiers: .command)
}
}
}
// 实现拖拽支持
import UIKit
class DraggableCollectionViewCell: UICollectionViewCell {
func setupDragSupport() {
let dragInteraction = UIDragInteraction(delegate: self)
addInteraction(dragInteraction)
}
}
extension DraggableCollectionViewCell: UIDragInteractionDelegate {
func dragInteraction(_ interaction: UIDragInteraction,
itemsForBeginning session: UIDragSession) -> [UIDragItem] {
guard let image = imageView.image else { return [] }
// 提供多种数据格式
let itemProvider = NSItemProvider()
// 图片数据
itemProvider.registerObject(image, visibility: .all)
// 图片 URL(如果有)
if let imageURL = imageURL {
itemProvider.registerObject(imageURL as NSURL, visibility: .all)
}
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = self // 本地拖拽时可以直接传递对象
return [dragItem]
}
}
最佳实践
如果你的 app 目前还不支持多窗口,在 Info.plist 中将 UIApplicationSupportsMultipleScenes 设为 YES,然后在 SceneDelegate 中正确处理每个 scene 的初始化。测试时在 Shelf 中打开 3-5 个窗口,检查内存和 CPU 是否正常。
键盘快捷键应该覆盖 app 的所有核心操作。优先实现 Cmd+N(新建)、Cmd+W(关闭)、Cmd+S(保存)这三个最常用的快捷键,然后逐步添加更多。
还有什么值得关注
- iPadOS 15 的
UIMenuSystem支持UIMenu.System.mainMenu,可以把菜单直接放在屏幕顶部的菜单栏中。 - SwiftUI 的
focusedSceneValue可以为键盘快捷键提供上下文相关的行为。 NSItemProvider支持canLoadObject方法,可以在拖拽接收前检查是否支持某种数据类型。