WWDC Quick Look 💓 By SwiftGGTeam
Adopt Quick Note

Adopt Quick Note

观看原视频

Highlight

Apple 让 Quick Note 复用 NSUserActivity 识别应用内容,开发者设置稳定标识符并处理活动恢复后,用户可以把当前内容链接进笔记,再从笔记回到应用里的同一内容。

核心内容

用户看到一页资料,想临时记一笔。过去常见流程是切到 Notes,复制标题,复制链接,再切回原来的应用。地点、文章、文档这类内容还要靠用户自己描述上下文。

01:15)Quick Note 把这个动作放到系统层。用户可以从屏幕右下角滑出快速备忘录浮窗,在浮窗顶部的 add link 菜单里添加当前网站或应用内容。浮窗可以移动、缩放,也可以收进屏幕边缘。

02:36)这些链接会和内容本身建立关系。用户回到同一个网站或应用内容时,Quick Note Suggestions 会在角落出现。点一下,系统打开之前那条笔记。

03:26)Apple 没有为 Quick Note 设计一套新链接协议。它使用已有的 NSUserActivity(用户活动)系统。应用把当前屏幕正在展示的内容注册为 activity,系统把它送给 Handoff、Spotlight、Reminders,也送给 Quick Note。

04:06)Quick Note 需要一个能长期匹配内容的标识符。这个标识符要唯一、跨设备可用、长期稳定。用户六个月后打开同一篇文章,系统仍然要能把笔记和文章匹配起来。

详细内容

1. 声明并创建 NSUserActivity

05:31)接入 Quick Note 有三步:在 Info.plist 声明支持的 activity type,创建并注册描述当前屏幕内容的 NSUserActivity,处理外部传入的 activity 恢复请求。第一步使用 NSUserActivityTypes 键,系统用它判断应用能否处理某个 activity。

import UIKit

struct Document {
    let title: String
    let stableIdentifier: String
}

func attachQuickNoteActivity(to viewController: UIViewController, document: Document) {
    let activity = NSUserActivity(activityType: "com.myapp.MyActivityType")
    activity.title = document.title
    activity.targetContentIdentifier = document.stableIdentifier
    activity.userInfo = ["documentIdentifier": document.stableIdentifier]
    viewController.userActivity = activity
}

关键点:

  • import UIKit 提供 UIViewController 和 responder 上的 userActivity 属性。
  • DocumenttitlestableIdentifier 表示当前屏幕内容。
  • attachQuickNoteActivity 把 activity 绑定到当前视图控制器。
  • NSUserActivity(activityType:) 使用应用在 NSUserActivityTypes 中声明过的类型字符串。
  • activity.title 是用户在 Quick Note 的 add link 菜单里看到的文字,适合直接使用文档或条目的标题。
  • activity.targetContentIdentifier 是 Quick Note 用来匹配内容的持久标识符。
  • activity.userInfo 保存恢复界面时需要的应用状态。
  • viewController.userActivity = activity 让 UIKit 管理当前 activity,不需要手动维护 current activity。

2. 选择稳定标识符

04:06)Quick Note 支持三个可作为链接标识的属性:targetContentIdentifierpersistentIdentifierwebpageURL。任意一个都可以让 Quick Note 生成链接。选择哪个属性,取决于应用还想支持哪些系统能力。

import Foundation

func configureIdentifiers(for activity: NSUserActivity, photoID: UUID, fallbackURL: URL) {
    let stableValue = "photo-\(photoID.uuidString)"

    activity.targetContentIdentifier = stableValue
    activity.persistentIdentifier = stableValue
    activity.webpageURL = fallbackURL
}

关键点:

  • configureIdentifiers 把同一个内容的标识集中配置,减少不同系统功能之间的差异。
  • photoID 表示应用保存的照片 UUID,满足长期稳定这个要求。
  • stableValue 没有使用本地文件路径、会话 ID、缩放比例这类临时信息。
  • targetContentIdentifier 可用于状态恢复,对 iPad 多任务体验有帮助。
  • persistentIdentifier 可用于系统 Spotlight 索引里的应用内容标识。
  • webpageURL 可作为回退链接;设备未安装应用时,可以打开对应网站。

3. 在 iOS 中处理 Quick Note 链接

07:15)用户点击 Quick Note 链接时,系统启动应用,并调用 scene delegate。willContinueUserActivityWithType 适合展示加载反馈;continue userActivity 用来搭建视图控制器并恢复状态;didFailToContinueUserActivityWithType 用于 Handoff 失败场景,Quick Note 不会调用这个失败方法,但完整接入 NSUserActivity 时可以实现。

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willContinueUserActivityWithType userActivityType: String) {
        // show user feedback while waiting for the NSUserActivity to arrive
    }

    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
        // set up view controllers and views to continue the activity
    }

    func scene(_ scene: UIScene, didFailToContinueUserActivityWithType userActivityType: String, error: Error) {
        // show error about failing to continue an activity
    }
}

关键点:

  • SceneDelegate 实现 UIWindowSceneDelegate,接收 scene 级别的 activity 恢复事件。
  • window 是恢复界面时可使用的窗口入口。
  • willContinueUserActivityWithType 在 activity 实体到达前触发,可显示加载提示。
  • continue userActivity 是打开 Quick Note 链接后的主恢复入口。
  • didFailToContinueUserActivityWithType 处理恢复失败反馈,主要服务 Handoff。

4. 在 macOS 中处理同一套 activity

08:04NSUserActivity 是跨平台 API。macOS 应用在 app delegate 中实现对应方法,流程与 iOS 相同:收到即将恢复的通知,拿到 activity 后恢复文档或视图,失败时给出提示。

import AppKit

class AppDelegate: NSObject, NSApplicationDelegate {
    func application(_ application: NSApplication, willContinueUserActivityWithType userActivityType: String) -> Bool {
        // show user feedback while waiting for the NSUserActivity to arrive
        return true
    }

    func application(_ application: NSApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([NSUserActivityRestoring]) -> Void) -> Bool {
        // set up view controllers or documents to continue the activity
        return true
    }

    func application(_ application: NSApplication, didFailToContinueUserActivityWithType userActivityType: String, error: Error) {
        // show error about failing to continue an activity, if appropriate
    }
}

关键点:

  • import AppKit 提供 macOS 应用生命周期类型。
  • AppDelegate 实现 NSApplicationDelegate,接收应用级 activity 恢复事件。
  • willContinueUserActivityWithType 返回 true,表示应用愿意继续该 activity。
  • continue userActivity 收到完整 activity,可打开文档或切换到目标视图。
  • restorationHandler 属于系统恢复流程的一部分,可传回参与恢复的对象。
  • didFailToContinueUserActivityWithType 负责恢复失败时的错误处理。

5. 用 needsSave 避免频繁写 userInfo

12:18)有些恢复状态变化很快。地图的中心点和缩放比例会随着手势不断变化。每次手势都重写 userInfo 会产生额外开销。Apple 建议设置 needsSave = true,等系统要把 activity 送给 Quick Note 或 Handoff 时,再在 userActivityWillSave 回调里写入最新状态。

import UIKit

class MapViewController: UIViewController {
    @IBOutlet var scrollView: UIScrollView!
    var visibleFrame: CGRect = .zero

    func markActivityForSaving(activity: NSUserActivity) {
        activity.needsSave = true
    }

    override func userActivityWillSave(_ userActivity: NSUserActivity) {
        userActivity.userInfo = [
            "center": NSStringFromCGPoint(CGPoint(x: visibleFrame.midX, y: visibleFrame.midY)),
            "zoomScale": scrollView.zoomScale
        ]
    }
}

关键点:

  • MapViewController 代表一个需要保存可见区域和缩放比例的界面。
  • scrollView 提供当前缩放比例。
  • visibleFrame 提供当前可见区域。
  • markActivityForSaving 只把 needsSave 标记为 true,避免在每次手势中重写状态字典。
  • userActivityWillSave 在系统需要保存 activity 前调用。
  • userInfo 写入 centerzoomScale,恢复时可以回到同一视野。
  • NSStringFromCGPoint 把中心点转成可放进 userInfo 的字符串。

6. 兼容旧链接和缺失内容

13:08)Quick Note 链接可能来自旧版本应用,也可能来自新版本应用。用户保存的内容还可能已经删除或移动。恢复 activity 时要能处理缺失字段、未知字段、内容不存在这些情况。

import Foundation

enum ActivityRestoreResult {
    case open(identifier: String)
    case showDeletedMessage
    case ignoreNewerVersion
}

func restoreResult(from userActivity: NSUserActivity, currentSupportedVersion: Int) -> ActivityRestoreResult? {
    let info = userActivity.userInfo ?? [:]
    let activityVersion = info["activityVersion"] as? Int ?? 1

    guard activityVersion <= currentSupportedVersion else {
        return .ignoreNewerVersion
    }

    guard let identifier = info["documentIdentifier"] as? String else {
        return nil
    }

    guard documentStillExists(identifier) else {
        return .showDeletedMessage
    }

    return .open(identifier: identifier)
}

func documentStillExists(_ identifier: String) -> Bool {
    true
}

关键点:

  • ActivityRestoreResult 把恢复结果拆成打开内容、显示删除提示、忽略新版本链接。
  • restoreResultuserInfo 读取恢复所需字段。
  • activityVersion 默认值为 1,便于兼容早期没有版本字段的链接。
  • guard activityVersion <= currentSupportedVersion 让旧应用遇到新格式链接时可以温和失败。
  • documentIdentifier 是打开内容的关键字段,缺失时返回 nil
  • documentStillExists 代表应用自己的内容存在性检查。
  • 内容已删除时返回 .showDeletedMessage,内容移动时可以在这里改成重定向逻辑。

核心启发

  • 做什么:给阅读应用增加“笔记回跳”能力。 为什么值得做:Quick Note 可以把当前文章和用户笔记绑定,用户重读文章时会看到笔记建议。 怎么开始:为文章详情页创建 NSUserActivity,用文章 UUID 设置 targetContentIdentifier,在 scene delegate 的 continue userActivity 中打开文章。

  • 做什么:给地图或旅行应用保存地点笔记。 为什么值得做:用户规划路线时可以把地点链接加入 Quick Note,之后回到地点页继续查看。 怎么开始:用地点的全局 ID 设置 persistentIdentifier,把地图中心点和缩放比例放进 userInfo,用 needsSave 延迟更新视图状态。

  • 做什么:给文档编辑器支持跨设备笔记链接。 为什么值得做NSUserActivity 同时服务 Quick Note 和 Handoff,用户在 iPad 记录文档笔记后,可以在 Mac 上回到同一个文档。 怎么开始:在 NSUserActivityTypes 注册文档查看 activity type,把文档 ID 写入 targetContentIdentifier,macOS 端实现 application(_:continue:restorationHandler:)

  • 做什么:给商品详情页加 Quick Note 回访入口。 为什么值得做:用户比较商品时会在笔记里保存多个链接,稳定标识符能让旧笔记继续打开正确商品。 怎么开始:用商品后台 ID 作为稳定值,设置 activity.title 为商品标题,内容下架时在恢复流程中显示明确提示。

  • 做什么:给有网站配套的应用提供 Web 回退。 为什么值得做webpageURL 可以让未安装应用的设备打开网页,链接仍有去处。 怎么开始:在设置 targetContentIdentifierpersistentIdentifier 的同时设置 webpageURL,确保 URL 不包含会话 ID 或临时筛选状态。

关联 Session

评论

GitHub Issues · utterances