Take your iPad apps to the next level
Swift & UI 进阶 26m

让你的 iPad app 更上一层楼

Take your iPad apps to the next level

2021年6月9日

在 Apple 官方观看视频

一句话判断

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 中。你需要实现 UIDragItemNSItemProvider 来提供标准化的数据格式(文本、图片、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 方法,可以在拖拽接收前检查是否支持某种数据类型。
WWDC 2021