Highlight
Apple 将 Shortcuts 带到 macOS,并让 AppKit 与 Mac Catalyst 应用通过 Intents 框架提供自定义 action,用户可以在 Shortcuts、菜单栏、键盘快捷键、Spotlight、AppleScript 和命令行中运行这些自动化能力。
核心内容
Mac 用户一直有自动化工具。AppleScript 可以控制应用,Shell Script 可以处理文件,Automator 可以把动作串成工作流。
问题是入口分散。一个普通用户想把待办 App、图片编辑 App、信息 App 串起来,往往要知道脚本、文件格式、应用事件。开发者也要为不同自动化入口写不同适配。
WWDC 2021 的变化是 Shortcuts 来到 macOS。用户打开 Mac 版 Shortcuts,会看到从 iPhone 同步过来的快捷指令。Shortcuts 编辑器也为 Mac 重做,支持 Mac 专属 action。
对开发者来说,关键变化是 Mac 应用可以像 iOS 应用一样提供 action。一个待办 App 暴露“创建任务”“查找任务”“删除任务”等操作后,用户可以把它接到 Notes、Messages、Files 或其他 App 后面,组成多步工作流。
Apple 没有要求开发者放弃旧的 Mac 自动化体系。Shortcuts 支持运行 AppleScript 和 Shell Script,也能导入大多数 Automator workflow。已有脚本可以继续用,新 action 可以接进同一个工作流。
从 App 功能到可组合 action
一个 action 最好是小的、明确的操作。演讲里的示例是协作任务管理 App。它的核心对象是 task,字段包括标题、截止日期、分享链接。围绕 task,可以提供创建、编辑、删除、查找、显示等动词。
这样拆分之后,用户可以从 Notes 选中文字,自动调整格式,输入截止日期,再调用 Create Task action。任务创建后,action 返回分享链接,后续步骤可以把链接发给别人。
从 iOS 到 Mac 的同一套 intent
macOS Monterey 让 Mac Catalyst App 使用同一套 Intents API。已有 iOS App 如果曾经在 Mac 版本中编译掉 Intents 集成,需要重新检查代码,把这部分能力在 macOS Monterey 上打开。
如果 App 同时发布 iOS 和 Mac 版本,Apple 建议两端编译同一个 intent definition 文件,并保持 intent 名称和参数一致。用户在一个平台创建的 shortcut,才能在另一个平台继续运行。
详细内容
定义 intent:把 action 当成函数设计
(09:56)实现 Shortcuts action 的第一步是在 Xcode 中创建 intent definition 文件。这个文件会随 App target 一起编译,Xcode 会根据它生成 Swift 类型和协议。
演讲里的 Create Task intent 有两个输入参数:title 和 dueDate。它还定义了一个 Task 类型,包含系统自动提供的 identifier、displayString,以及 App 自己添加的 dueDate 和 URL。
// Intent definition 生成的类型会在构建后出现在代码中
let intent = CreateTaskIntent()
intent.title = "Review pull request"
intent.dueDate = DateComponents(calendar: .current, year: 2021, month: 6, day: 8, hour: 15)
关键点:
CreateTaskIntent来自.intentdefinition文件,不需要手写类型声明。title是创建任务所需的文本参数。dueDate使用 Date Components 类型,对应演讲中为截止日期选择的参数类型。- 构建项目后,Xcode 会生成
Task类型和CreateTaskIntent类型。
(13:17)Shortcuts app 区域还要配置 input parameter 和 summary。演讲中把 title 设为 input parameter。这样用户把上一段文字接到 Create Task action 时,标题会自动填入。
Summary: Create task with due date
Input parameter: title
Required parameters: title, dueDate
关键点:
Summary是用户在 Shortcuts 编辑器里看到的动作摘要。Input parameter决定上一步输出默认填到哪个参数。- 必填参数放进摘要后,用户不用展开 action 就能看到关键输入。
分发 intent:让 macOS 把 action 交给 App
(15:15)Mac App 处理 intent 有两种方式。第一种是在 App 内处理,intent 会送到 App delegate。第二种是建立 Intents extension,用轻量独立进程处理。
Apple 建议大多数 App 从 in-app intent handling 开始。这样 action 可以直接修改 App 状态。只有当启动完整 App 成本太高时,再考虑 extension。
import SwiftUI
import Intents
@main
struct SouperTaskApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
func application(_ application: NSApplication, handlerFor intent: INIntent) -> Any? {
if intent is CreateTaskIntent {
return IntentHandler()
}
return nil
}
}
关键点:
import Intents引入 SiriKit 所在的 Intents 框架。@NSApplicationDelegateAdaptor把AppDelegate接入 SwiftUI App 生命周期。application(_:handlerFor:)是 macOS Monterey 中NSApplicationDelegate可用的 intent 分发入口。intent is CreateTaskIntent判断当前 intent 是否由 Create Task action 触发。IntentHandler()是负责处理该 intent 的对象。return nil表示 App 不处理其他 intent。
解析参数:在执行前要求用户补齐输入
(18:32)Intent handler 要遵守 Xcode 生成的协议。Create Task intent 对应 CreateTaskIntentHandling。协议会包含 resolve、provide options、confirm、handle 四类方法。
resolve 用来检查单个参数。Create Task 有两个必填参数,标题和截止日期,所以生成两个 resolve 方法。
class IntentHandler: NSObject, CreateTaskIntentHandling {
func resolveTitle(for intent: CreateTaskIntent, with completion: @escaping (INStringResolutionResult) -> Void) {
guard let title = intent.title, !title.isEmpty else {
return completion(.needsValue())
}
return completion(.success(with: title))
}
func resolveDueDate(for intent: CreateTaskIntent, with completion: @escaping (CreateTaskDueDateResolutionResult) -> Void) {
guard let dateComponents = intent.dueDate else {
return completion(.needsValue())
}
return completion(.success(with: dateComponents))
}
}
关键点:
IntentHandler遵守CreateTaskIntentHandling,这是 intent definition 生成的协议。resolveTitle检查intent.title是否存在且非空。.needsValue()告诉系统需要向用户询问这个值。.success(with: title)表示标题参数通过检查。resolveDueDate检查截止日期是否存在。CreateTaskDueDateResolutionResult是截止日期参数对应的解析结果类型。
自定义校验:拒绝过去的日期
(19:37)演讲中的 App 要求任务截止日期必须在未来。开发者先在 intent definition 文件中添加自定义错误 invalidDate,再在 resolve 方法里返回 unsupported。
func resolveDueDate(for intent: CreateTaskIntent, with completion: @escaping (CreateTaskDueDateResolutionResult) -> Void) {
guard
let dateComponents = intent.dueDate,
let dueDate = Calendar.current.date(from: dateComponents)
else {
return completion(.needsValue())
}
if dueDate < Date() {
return completion(.unsupported(forReason: .invalidDate))
}
return completion(.success(with: dateComponents))
}
关键点:
intent.dueDate取出用户输入的 Date Components。Calendar.current.date(from:)把 Date Components 转成可比较的Date。guard失败时返回.needsValue(),让用户补充日期。dueDate < Date()判断日期是否早于当前时间。.unsupported(forReason: .invalidDate)返回 intent definition 中定义的错误原因。.success(with: dateComponents)表示日期可以用于执行 action。
执行 action:创建任务并返回输出
(20:34)handle 方法执行真正的业务逻辑。演讲中它读取标题和截止日期,创建任务,构造响应对象,再把创建出的 task 放进响应。
class IntentHandler: NSObject, CreateTaskIntentHandling {
func handle(intent: CreateTaskIntent, completion: @escaping (CreateTaskIntentResponse) -> Void) {
let title = intent.title!
let dueDate = intent.dueDate!
let task = createTask(name: title, due: dueDate)
let response = CreateTaskIntentResponse(code: .success, userActivity: nil)
response.task = task
completion(response)
}
}
关键点:
handle会在参数解析完成后调用。intent.title!和intent.dueDate!读取已通过 resolve 的参数。createTask(name:due:)代表 App 内部创建任务的业务逻辑。CreateTaskIntentResponse(code: .success, userActivity: nil)构造成功响应。response.task = task把新建任务作为 action 输出。completion(response)把结果交还给 Shortcuts,后续 action 可以读取 task 的属性,例如分享链接。
文件型 App:用 file parameter 处理文档
(23:24)iOS 15 和 macOS Monterey 的 intent file parameter 允许用户选择文件,并把文件传给 App 的 action。文档型 App 可以围绕文件设计 action。
演讲给出的例子是电子表格 App。它可以提供打开文档的 action,也可以提供向某个文件的一行添加数据的 action。Sound Analysis session 也展示了第三方 Shortcuts 如何处理文件。
func resolveDocument(for intent: AddRowIntent, with completion: @escaping (INFileResolutionResult) -> Void) {
guard let file = intent.document else {
return completion(.needsValue())
}
return completion(.success(with: file))
}
关键点:
intent.document表示 Shortcuts 传入的文件参数。INFileResolutionResult用来返回文件参数的解析结果。.needsValue()要求用户选择文件。.success(with: file)表示文件可以传给后续处理逻辑。- 这类 action 适合文件存在磁盘上的文档型 App。
从 AppleScript 运行 Shortcuts
(25:39)Shortcuts 暴露了 scripting interface。AppleScript 可以通过 Shortcuts Events 进程按名称运行一个快捷指令。
tell application "Shortcuts Events"
run the shortcut whose name is "Make GIF"
end tell
关键点:
Shortcuts Events是 Shortcuts 的脚本接口进程。run the shortcut触发一个已有快捷指令。whose name is "Make GIF"按用户创建的 shortcut 名称查找目标。- 旧 AppleScript 工作流可以用这种方式调用新 Shortcuts。
从 Swift 或 Objective-C 运行 Shortcuts
(25:49)Mac App 可以用 Scripting Bridge 调用同一个 Shortcuts Events 进程。沙盒 App 还需要添加 com.apple.security.scripting-targets entitlement,并加入 com.apple.shortcuts.run target。
import ScriptingBridge
@objc protocol ShortcutsEvents {
@objc optional var shortcuts: SBElementArray { get }
}
@objc protocol Shortcut {
@objc optional var name: String { get }
@objc optional func run(withInput: Any?) -> Any?
}
extension SBApplication: ShortcutsEvents {}
extension SBObject: Shortcut {}
guard
let app: ShortcutsEvents = SBApplication(bundleIdentifier: "com.apple.shortcuts.events"),
let shortcuts = app.shortcuts else {
print("Couldn't access shortcuts")
return
}
guard let shortcut = shortcuts.object(withName: "Make GIF") as? Shortcut else {
print("Shortcut doesn't exist")
return
}
_ = shortcut.run?(withInput: nil)
关键点:
import ScriptingBridge引入 Scripting Bridge 框架。ShortcutsEvents协议声明shortcuts集合。Shortcut协议声明name和run(withInput:)。SBApplication(bundleIdentifier: "com.apple.shortcuts.events")连接到 Shortcuts Events。app.shortcuts取得用户已有的 shortcut 列表。shortcuts.object(withName: "Make GIF")查找指定名称的快捷指令。shortcut.run?(withInput: nil)运行快捷指令,并传入空输入。
核心启发
-
做什么:给任务管理 App 增加“从选中文本创建任务”action。 为什么值得做:演讲中的 Create Task action 证明,Shortcuts 可以把上一段文本自动填入 intent input parameter。 怎么开始:创建 intent definition,定义
title和dueDate参数,把title设为 input parameter,实现CreateTaskIntentHandling的 resolve 与 handle。 -
做什么:给文档 App 增加“对指定文件执行处理”action。 为什么值得做:iOS 15 和 macOS Monterey 支持 intent file parameter,用户可以从 Files 或 Finder 流程中把文件传给 App。 怎么开始:为 action 添加文件参数,在 handler 中用
INFileResolutionResult校验文件,再在handle中读取文件并返回新文件。 -
做什么:给图片或音频 App 提供一组可组合的批处理 action。 为什么值得做:Shortcuts 可以把第三方 action 接到系统 action 后面,例如选择文件、处理内容、分享结果。 怎么开始:先挑 3 个原子操作,例如“应用滤镜”“导出文件”“生成分享链接”,分别建成 intent,并为每个 response 定义可被后续步骤使用的输出。
-
做什么:在 Mac App 里提供“运行用户指定 shortcut”的入口。 为什么值得做:Shortcuts 提供
Shortcuts Eventsscripting interface,App 可以列出并运行用户已有快捷指令。 怎么开始:引入ScriptingBridge,用SBApplication(bundleIdentifier: "com.apple.shortcuts.events")连接 Shortcuts Events,按名称找到 shortcut 后调用run(withInput:)。 -
做什么:让 iOS App 的 Shortcuts action 同步支持 Mac。 为什么值得做:macOS Monterey 的 Mac Catalyst App 可以使用同一套 Intents API,用户在 iPhone 创建的 shortcut 可以在 Mac 继续使用。 怎么开始:检查 Mac target 是否编译同一个 intent definition,确认 intent 名称和参数在 iOS 与 Mac 版本中保持一致。
关联 Session
- Donate intents and expand your app’s presence — 讲 intent donation 如何让系统在合适场景展示 App action。
- Design great actions for Shortcuts, Siri, and Suggestions — 讲如何设计粒度合适、能被多步 shortcut 组合的 action。
- Discover built-in sound classification in SoundAnalysis — 演讲中提到该 session 展示第三方 Shortcuts 如何处理文件型输入。
- What’s new in AppKit — 了解 macOS Monterey 中原生 Mac App 的开发基础能力。
评论
GitHub Issues · utterances