设计优秀的 App Intents 体验
Design App Intents for system experiences
2024年6月10日
一句话判断
iOS 18 把 App Intents 的定位从”少数常用操作暴露给系统”变成了”你 App 做的每件事都应该是一个 Intent”——这意味着 App Intents 的设计质量直接决定了你的 App 在 Spotlight、Shortcuts、Siri、Action Button 等系统入口的表现。
这场 Session 讲了什么
App Intents 是让你的 App 功能被系统发现的机制。去年它被定位为”最常用的几个操作”暴露给 Shortcuts 和 Siri,今年 Apple 大幅调整了策略:你的 App 做的所有事情都应该是 App Intent。
这背后的逻辑是系统的各种入口(Spotlight、Action Button、Siri、Controls、Widgets)越来越多,每个入口都需要 App 提供足够丰富的 Intent 才能发挥作用。如果你的 App 只有 3 个 Intent,用户在 Shortcuts 里能组合的空间就很有限。
但”做更多 Intent”不等于”随便做”。Session 花了大部分时间讲设计原则:怎么拆分 Intent 才既灵活又清晰、参数怎么设计才能在 Shortcuts 里读起来像人话、什么时候应该自动打开 App、什么时候该让用户确认。这些都是从大量第三方 App 的实践中提炼出来的。
值得深挖的点
从”少而精”到”全而灵活”
去年 Apple 的建议是”只暴露最常用的操作”。今年改成了”anything your app does should be an app intent”。这个转变不是拍脑袋,而是因为 Shortcuts 的使用模式已经证明了:用户喜欢把不同 App 的 Intent 组合成意想不到的工作流。
但”全”不等于”碎”。Session 举了一个反面例子:如果你的 App 有不同类型的提醒事项,不要为每种类型做一个单独的 Intent(“打开工作提醒”、“打开个人提醒”、“打开购物提醒”)。应该做一个”打开提醒”Intent,把提醒类型作为参数。这样用户既可以直接调用,也可以在 Shortcuts 里动态配置参数。
另一个常见的坑是”为 UI 元素做 Intent”——比如做一个”点击取消按钮”的 Intent。这完全错误,因为 Intent 描述的应该是用户的任务(“保存草稿”、“删除草稿”),而不是界面上的操作。用户在 Shortcuts 里看到”点击取消按钮”会觉得莫名其妙。
参数摘要:读起来要像一句话
App Intent 的参数摘要(Parameter Summary)是用户在 Shortcuts 里看到的描述。它必须读起来像一个完整的句子,不管参数怎么变。
Session 用相机 App 做例子:“Open camera to [panorama] mode”——不管参数选什么,这句话都是通顺的。
参数类型的选择也很讲究:简单的输入(数字、文本、日期)用内置类型;选项固定的(Tab 选择)用静态参数;选项动态变化的(文件夹列表)用 App Entity 作为动态参数。
Optional vs Required 的权衡:Optional 参数允许 Intent 在未配置时直接执行,不打断用户。比如”打开笔记”Intent 的文件夹参数如果是 Optional,没指定时就打开全部笔记视图。Required 参数则每次都会追问,适合”搜索邮件”这种没有输入就毫无意义的场景。
Toggle 参数是二值状态的利器——手电筒的”开/关”如果不支持 Toggle,用户每次执行都要选一下开还是关,这很蠢。加上 Toggle 支持,Intent 默认切换当前状态,只在需要明确时才问。
代码片段
灵活的 App Intent 结构
struct OpenReminderIntent: AppIntent {
static var title: LocalizedStringResource = "Open Reminders"
static var description = IntentDescription("Open a specific reminder list")
// Optional 参数:不指定就打开全部列表
@Parameter(title: "List", default: nil)
var list: ReminderListEntity?
// 参数摘要读起来像一句话
static var parameterSummary: some ParameterSummary {
Summary("Show \(\.$list) reminders")
}
func perform() async throws -> some IntentResult {
// 如果指定了列表就打开该列表,否则打开全部
if let list = list {
// 打开特定列表
} else {
// 打开全部列表视图
}
return .result()
}
}
场景:一个灵活的提醒事项 Intent,支持打开特定列表或全部列表。坑:参数摘要里的 \.list 在 Optional 为 nil 时会显示 “all”,确保这个默认展示也合理。
支持 Toggle 的二值状态 Intent
struct SetFlashlightIntent: AppIntent {
static var title: LocalizedStringResource = "Set Flashlight"
@Parameter(title: "State", default: .toggle)
var state: FlashlightState
static var parameterSummary: some ParameterSummary {
Summary("Turn \(\.$state) the flashlight")
}
func perform() async throws -> some IntentResult {
// Toggle 模式下自动切换当前状态
flashlight.set(state)
return .result()
}
}
场景:手电筒开关,支持 Toggle 免询问。坑:只有真正的二值状态才适合用 Toggle,三值以上的状态不要强行用。
打开 App 展示结果
struct CreateBoardIntent: AppIntent {
static var title: LocalizedStringResource = "Create Board"
static var openAppWhenRun: Bool = true // 默认打开 App
@Parameter(title: "Name")
var name: String
func perform() async throws -> some IntentResult & OpensIntent {
let board = Board.create(name: name)
// 直接打开新创建的画板,不加额外动画
return .result(opensIntent: OpenBoardIntent(board: board))
}
}
场景:创建画板后自动打开 App 展示结果,用户可以立刻开始操作。坑:openAppWhenRun 默认是 true,如果你的 Intent 在 Shortcuts 流程中不需要打开 App,要显式设为 false。
最佳实践
已有项目迁移:如果你已经有几个 App Intent,审查一下它们是否犯了”碎拆”的问题——多个 Intent 做同一件事应该合并为一个带参数的 Intent。检查参数摘要是否在任何参数组合下都读得通。对于二值状态的 Intent,加上 Toggle 支持。
新项目起步:从动词出发(打开、创建、搜索、设置、删除)列出你 App 的所有功能,每个功能一个 Intent。优先实现最常用的 5-10 个,确保参数摘要和描述文本到位。不要忘了处理”在后台运行”的场景——如果你的 App 支持 Live Activity、音频播放或录音,对应的 Intent 应该不需要打开 App 就能执行。
还有什么值得关注
- iOS 18 中
openAppWhenRun默认开启,这是一个行为变化——如果你的 Intent 之前不需要打开 App,需要显式关闭。 - Shortcuts 里用户可以关闭单个 Intent 的”打开 App”行为,让你的 Intent 在自动化流程中不中断。
- App Entity 作为动态参数可以让你的 Intent 选项随用户数据变化自动更新,不需要手动维护。