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属性。Document用title和stableIdentifier表示当前屏幕内容。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 支持三个可作为链接标识的属性:targetContentIdentifier、persistentIdentifier、webpageURL。任意一个都可以让 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:04)NSUserActivity 是跨平台 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写入center和zoomScale,恢复时可以回到同一视野。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把恢复结果拆成打开内容、显示删除提示、忽略新版本链接。restoreResult从userInfo读取恢复所需字段。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可以让未安装应用的设备打开网页,链接仍有去处。 怎么开始:在设置targetContentIdentifier或persistentIdentifier的同时设置webpageURL,确保 URL 不包含会话 ID 或临时筛选状态。
关联 Session
- Meet Shortcuts for macOS — 了解应用操作如何进入 macOS 的系统自动化流程。
- Donate intents and expand your app’s presence — 学习把应用能力捐赠给系统,让系统在合适场景展示。
- Showcase app data in Spotlight — 继续了解
persistentIdentifier关联的 Spotlight 数据展示和搜索。 - Take your iPad apps to the next level — 配合 Quick Note,改进 iPad 上的多任务和内容恢复体验。
评论
GitHub Issues · utterances