这篇能做出什么
做完这篇教程,你的 App 会具备三种「被系统调用」的能力:
1. 用户在 快捷指令(Shortcuts) App 里能搜到你的动作,可以拖进自动化流程;
2. 用户对着 Siri 说一句「用 XX 记一条待办」,Siri 直接调用你 App 里的代码,把数据写进去,再回一句话确认,全程不需要打开 App;
3. 在支持 Apple 智能的设备上,系统能按语义理解用户意图,把「帮我记一下明天下午三点开会」这类自然语言路由到你声明的动作上。
举个具体例子:一个叫「清单盒子」的待办 App,做完之后用户可以说「用清单盒子加个任务」,Siri 追问「内容是什么」,用户说「买牛奶」,App 在后台写入「收件箱」清单,Siri 回答「已添加到收件箱」。整个链路的核心就是三个东西:App Intent(动作)、App Entity(可选参数对象)、App Shortcuts(Siri 短语)。
前置条件清单
动手前先对照一遍:
- 一台 macOS,装有 Xcode(版本以官方文档当前版本为准,App Intents 需要 Xcode 14 及以上);
- 一个能编译运行的 iOS App 工程,部署目标至少 iOS 16(App Intents 的起点;Apple 智能相关能力需要更高系统版本,以官方文档为准);
- 一台真机(调试 Siri 语音强烈建议真机,模拟器上 Siri 行为受限,但用快捷指令 App 触发动作是可行的);
- Apple Developer 账号(真机调试需要签名);
- 设备上已开启 Siri,并在「设置 → Siri 与搜索」里确认语言与你的短语一致;
- 心里想清楚一个具体、边界清晰的动作,比如「添加一条待办」「查询今天还剩几件事」。不要一上来就做「万能助手」,那是调不通的。
第一步:写一个最小可用的 App Intent
先别管 Siri,先让动作本身能跑起来。新建一个 Swift 文件,比如 AddTaskIntent.swift:
```swift
import AppIntents
struct AddTaskIntent: AppIntent {
// Siri 和快捷指令里显示的名字,越短越好
static var title: LocalizedStringResource = "添加待办"
// 给系统理解用的描述,写清楚"做什么"
static var description = IntentDescription("把一条待办事项加入指定清单")
// 这个动作不需要打开 App,在后台完成
static var openAppWhenRun: Bool = false
@Parameter(title: "内容")
var text: String
@Parameter(title: "清单")
var listName: String?
func perform() async throws -> some IntentResult & ProvidesDialog {
let target = listName ?? "收件箱"
try TaskStore.shared.add(text: text, to: target)
return .result(dialog: "已添加到\(target)")
}
}
```
几个必须知道的点:
@Parameter修饰的属性必须是var。非可选参数如果没有默认值,系统会在运行时追问用户;可选参数(String?)则可以跳过。perform()是async throws的,耗时操作放这里没问题,但要控制时长,见后面「常见坑」。- 返回
.result(dialog:)才能让 Siri 说出确认语;只返回.result()时 Siri 可能只回一句通用的「完成了」。 openAppWhenRun默认是false。如果你需要用户看到界面再操作,把它设成true。
第二步:注册成 Siri 听得懂的短语
Intent 写完了,系统还不知道该在什么话术下调用它。这就要靠 AppShortcutsProvider:
```swift
import AppIntents
struct TaskAppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: AddTaskIntent(),
phrases: [
"用 \(.applicationName) 记一条待办",
"在 \(.applicationName) 里加个任务",
"让 \(.applicationName) 帮我记一下"
],
shortTitle: "记待办",
systemImage: "checklist"
)
}
}
```
规矩很硬,踩了就不生效:
- 每条短语必须包含
\(.applicationName)。这是系统用来识别「这句话属于哪个 App」的锚点,缺了直接编译报错。 - 一条
AppShortcut至少一句短语,建议 2~4 句,覆盖不同说法。 - 短语要以动词开头、口语化、短。像「用 XX 添加一条待办事项到清单」这种书面长句,识别率会明显下降。
- 每个 App 能注册的 App Shortcut 数量有上限(官方文档写明,数量不多),所以优先挑真正高频的动作。
- 这个
struct放在主 App target 里,不要放进扩展。
第三步:让参数可挑选(AppEntity + EntityQuery)
上面 listName 是裸字符串,用户得自己念清单名,很容易念错。更好的做法是把「清单」定义成一个实体:
```swift
import AppIntents
struct TaskListEntity: AppEntity {
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "清单")
static var defaultQuery = TaskListQuery()
var id: String
var name: String
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name)")
}
}
struct TaskListQuery: EntityQuery {
// 系统拿着已有 id 回查实体
func entities(for identifiers: [String]) async throws -> [TaskListEntity] {
identifiers.compactMap { TaskStore.shared.list(withID: $0) }
}
// Siri 追问"哪个清单"时给出的候选
func suggestedEntities() async throws -> [TaskListEntity] {
TaskStore.shared.allLists()
}
// 用户没指定时的默认值
func defaultResult() async -> TaskListEntity? {
TaskStore.shared.allLists().first
}
}
```
然后把参数改成:
```swift
@Parameter(title: "清单")
var list: TaskListEntity?
```
这样 Siri 会读出候选清单让用户选,而不是靠语音识别猜字符串。同理,如果有「优先级」这种固定选项,用 AppEnum 比用字符串靠谱得多——系统能直接理解 AppEnum 的取值,语义路由也更准。
第四步:暴露给 Apple 智能与系统域
在支持 Apple 智能的系统上,除了照短语匹配,系统还会按语义理解来路由请求。要吃到这波能力,需要做两件事:
一是把动作描述清楚。 系统看不懂你的业务黑话,只能看你给的元数据。检查清单:
title用动词短语,别用名词(「添加待办」而不是「待办事项」);description一句话讲清楚输入输出;- 参数全部用系统能理解的类型:基础类型、
AppEnum、AppEntity,不要用自定义struct; - 返回
dialog,让系统知道动作成功了。
二是声明所属的系统域(domain)。 系统为相册、邮件、笔记等常见场景预定义了一批 schema,实现对应的协议能让你的动作被更高层的能力直接调度:
```swift
import AppIntents
// schema 名称在 Xcode 里输入 "." 会触发补全,
// 具体可用列表以官方文档的 App Intent domains 页面为准
@AssistantIntent(schema: .yourDomain.yourAction)
struct OpenTaskIntent: AppIntent {
static var title: LocalizedStringResource = "打开待办"
@Parameter(title: "待办")
var task: TaskEntity
func perform() async throws -> some IntentResult {
AppRouter.shared.openTask(id: task.id)
return .result()
}
}
```
如果暂时没有匹配到你业务的 schema,也不必强求——把普通的 AppIntent + AppShortcut 做好,Siri 和快捷指令的可用性已经足够覆盖大部分场景。schema 的具体命名与可用范围请以官方文档为准,不同系统版本差异较大。
第五步:处理权限与运行时
App Intents 本身不需要额外申请权限,但动作里调用的系统能力需要。这是新手最容易漏的一环:快捷指令里跑得好好的,一装到别人手机上就返回失败。
- 涉及通讯录、相册、日历、提醒事项等,先在
Info.plist里写好对应的 usage description; - 在
perform()里做一次状态检查,没授权就抛出带说明的错误:
```swift
func perform() async throws -> some IntentResult & ProvidesDialog {
let status = await ContactsStore.authorizationStatus()
guard status == .authorized else {
throw IntentError.permissionDenied
}
// ...
return .result(dialog: "已保存")
}
```
自定义错误建议实现 LocalizedError,errorDescription 写成人话(「需要通讯录权限,请在设置中开启」),否则用户只会看到一句「出错了」。
- 如果动作需要展示界面,就设
static var openAppWhenRun = true,并把 UI 相关代码标@MainActor。
第六步:调试与验证
调试顺序建议从「最可控」到「最真实」:
1)用单元测试直接调 perform()。 这能最快验证业务逻辑,绕开所有系统层:
```swift
import XCTest
@testable import TaskApp
final class AddTaskIntentTests: XCTestCase {
func testAddTask() async throws {
let intent = AddTaskIntent()
intent.text = "买牛奶"
intent.list = TaskListEntity(id: "shopping", name: "购物")
let result = try await intent.perform()
XCTAssertTrue(TaskStore.shared.list(withID: "shopping")?.items.contains("买牛奶") ?? false)
}
}
```
2)在快捷指令 App 里手动触发。 装好 App 并至少启动一次,打开「快捷指令」→ 搜索你的 App 名字,应该能看到动作卡片。拖一个出来点运行,这一步能验证 Intent 注册、参数解析、返回值是否正常。
3)用 Siri 语音触发。 真机上直接念短语。如果听错,把短语改短、换更常见的动词。
4)下断点。 在 Xcode 里先把 App 跑起来(或用 Debug → Attach to Process 附着到已安装的 App),再从快捷指令触发动作,断点就能命中 perform()。
5)看日志。 用 OSLog 打点,在 Console.app 里按 subsystem 过滤:
```swift
import OSLog
let logger = Logger(subsystem: "com.example.taskapp", category: "intents")
func perform() async throws -> some IntentResult & ProvidesDialog {
logger.info("AddTaskIntent 收到参数 text=\(text, privacy: .public)")
// ...
}
```
常见坑与排错
短语写好了但 Siri 完全没反应。
先确认短语里有没有 \(.applicationName)(没有的话根本编译不过);再确认 App 真的装到了这台设备上,并且启动过一次。还有一个高频原因:App Shortcuts 会被系统缓存。改了短语之后,删掉 App 重新安装一次,通常就能刷新。
快捷指令里搜不到我的动作。
检查 AppShortcutsProvider 是否放在主 target、appShortcuts 是不是 static var 返回数组。另外,只有被 AppShortcut 引用过的 Intent 才会自动出现在快捷指令库里;没有注册的 Intent 需要用户手动「新建快捷指令 → 搜索 App」。
Siri 说「抱歉,我没听懂」。
多半是短语太书面或太长。改成「用 X 记一下」这类 5~8 个字的口语短句;同时确认 Siri 语言和你的短语语言一致(中英文混着说很容易失败)。
参数选不出来,或者一运行就报参数错误。
自定义类型没有实现 AppEntity + EntityQuery,或者忘了给 defaultQuery。系统拿不到候选列表,只能报错。
Siri 回了「已完成」,但数据没写进去。
perform() 可能抛了错但被吞掉了。检查有没有实现 LocalizedError,把错误信息透出来;同时确认 App 是不是在被杀死状态下被唤起——这时候单例、数据库连接可能还没初始化好,需要在 perform() 里做懒加载。
动作跑到一半超时。
perform() 适合秒级完成的工作。上传大文件、批量同步这类操作,正确做法是立刻返回 .result(dialog: "已开始同步"),把重活丢到后台任务或本地通知,完成后再告知用户。
模拟器上 Siri 不好使。
这属于正常现象。模拟器上优先用快捷指令 App 触发同一个 Intent 来验证逻辑,语音环节留给真机。
改了代码但行为没变。
除了删 App 重装,也要注意 Xcode 的增量安装有时不会刷新 Intent 元数据。遇到玄学问题,先 Clean Build Folder,再卸载重装。
下一步建议
链路跑通之后,可以按这个顺序继续加厚:
1. 加 SiriTipView,在 App 首页把常用短语提示给用户,比任何新手引导都有效;
2. 做参数追问体验:给 @Parameter 加上追问文案,让 Siri 的问法更像人话;
3. 接入更多系统入口:同一批 App Intent 可以同时供小组件、控制中心控件、操作按钮使用,写一次多处复用;
4. 做 Spotlight 索引:让实体能被系统搜索到,语义路由的命中率会明显提升(相关协议以官方文档为准);
5. 补本地化:短语和 title 都在 String Catalog 里翻译,否则换语言环境就废了;
6. 观察线上表现:给 Intent 的入口、成功、失败分别打点,跑一段时间再回来优化短语和参数设计。
最后提醒一句:App Intents 的可用能力与系统版本强绑定,本文涉及的 API 名称、数量上限、schema 列表都可能随版本演进,动手时请以官方文档当前版本为准。
