App Intents 框架进阶
Explore new advances in App Intents
2025年6月9日
一句话判断
App Intents 终于从”系统替你展示一个静态结果”进化到了”用户可以直接在 Snippet 里交互”——按钮、Toggle、多选项、撤销手势全部打通,而且 App Intent 的代码可以完全不做修改就用在 Snippet 里。
这场 Session 讲了什么
这次 App Intents 的升级覆盖面很广,核心变化是 Interactive Snippets。Snippet 之前只能展示确认信息或结果,现在可以嵌入按钮和 Toggle,用户点击后直接触发已有的 App Intent,系统会在后台执行 Intent 然后刷新 Snippet 视图。整个过程不需要你写额外的胶水代码——已有的 Intent 直接复用。
除了 Snippet,还有一系列系统集成方面的更新:Visual Intelligence(图像搜索)现在可以通过 IntentValueQuery + SemanticContentDescriptor 让你的 App 搜索结果出现在视觉搜索面板里;Spotlight Mac 上可以直接执行 Action,配合 IndexedEntity + PredictableIntent 可以大幅提升被建议的概率;On-screen Entity 通过 NSUserActivity 把当前视图内容关联给 ChatGPT,支持 PDF/富文本/纯文本三种 Transferable 格式。
开发者体验方面的改进也不少:ComputedProperty 宏避免在 AppEntity 上重复存储值;DeferredProperty 宏让昂贵的属性只在被显式请求时才计算;App Intents 现在可以放在 Swift Package 和静态库里了。
值得深挖的点
Snippet Intent 的生命周期与性能陷阱
SnippetIntent 遵循 SnippetIntent 协议,它的 perform 方法返回 ShowsSnippetView。关键约束是:SnippetIntent 会被系统反复创建和执行——每次交互触发、设备状态变化(比如切换深色模式)都会重新运行 perform。所以 perform 里不能修改 App 状态,而且视图渲染必须快。
参数处理也有讲究:标有 @Parameter 的属性在每次 perform 时都会被系统重新填充。对于 AppEntity 类型,系统会重新从 Query 里取最新值——这保证了数据新鲜度,但也意味着 Query 本身要快。对于非 AppEntity 的值,只应在创建 SnippetIntent 时指定一次,因为之后不会被刷新。
UndoableIntent 与 Undo 栈的整合
UndoableIntent 协议提供了一个 undoManager 属性,系统会自动把最相关的 undo manager 注入进来,即使 Intent 是在 Extension 里运行的。这意味着你的 App Intent 撤销操作和 UI 层的撤销操作会按正确的顺序排列在同一个栈里——用户用三指左滑就能撤销 Siri 执行的操作,体验非常自然。
Supported Modes 与动态前台化
Supported Modes 允许 Intent 根据用户场景决定是否前台化 App。三种前台模式:immediate(系统在执行前先拉起 App)、dynamic(Intent 自己决定)、deferred(Intent 后续会前台化但不是立即)。dynamic 模式配合 continueInForeground 方法可以在运行时精确控制前台化时机——比如检查到景点已关闭就不前台化,直接返回语音结果。
代码片段
返回交互式 Snippet
struct FindLandmarkIntent: AppIntent {
static var title: LocalizedStringResource = "Find Closest Landmark"
func perform() async throws -> some IntentResult & ShowsSnippetIntent {
let landmark = findClosestLandmark()
return .result(
snippet: LandmarkSnippetIntent(landmark: landmark)
)
}
}
struct LandmarkSnippetIntent: SnippetIntent {
@Parameter var landmark: LandmarkEntity
func perform() async throws -> some IntentResult & ShowsSnippetView {
let isFavorite = await checkFavorite(landmark)
return .result(view: LandmarkView(
landmark: landmark,
isFavorite: isFavorite
))
}
}
坑:SnippetIntent 的视图里用 Button 关联 AppIntent 时,系统会执行那个 Intent 并等待完成,然后用原始 SnippetIntent 触发一次 perform 来刷新视图。如果关联的 Intent 也返回了 Snippet,会替换掉当前的 Snippet(仅限 result 类型的 Snippet)。
Image Search 支持
struct LandmarkQuery: IntentValueQuery {
func values(for input: SemanticContentDescriptor) async throws -> [LandmarkEntity] {
let cgImage = try await input.cgImage()
// 用 Vision 框架或其他方式搜索匹配的地标
let results = await searchLandmarks(image: cgImage)
return results
}
}
struct OpenLandmarkIntent: OpenIntent {
static var title: LocalizedStringResource = "Open Landmark"
@Parameter var target: LandmarkEntity
func perform() async throws -> some IntentResult {
// 导航到地标详情页
return .result()
}
}
坑:OpenIntent 的 target 参数类型必须和 Query 返回的 Entity 类型一致,否则你的 App 不会在搜索结果里出现。
Dynamic Supported Mode
struct GetCrowdStatusIntent: AppIntent {
static var supportedModes: IntentModes = [.background, .foreground(.dynamic)]
func perform() async throws -> some IntentResult & ReturnsValue<String> {
let landmark = try await fetchLandmark()
guard landmark.isOpen else {
return .result(value: "景点已关闭")
}
if systemContext.canForegroundApp {
try await continueInForeground(alwaysConfirm: false)
return .result(value: "已跳转到详情页")
}
let crowd = try await getCrowdLevel(landmark)
return .result(value: "当前拥挤度:\(crowd)")
}
}
坑:continueInForeground 抛出异常时表示前台化被系统或用户拒绝,不要 catch 它——直接让 perform 终止。
最佳实践
Snippet 的设计原则是”轻量、即时、可扫视”。Session 特别强调 Snippet 高度不要超过 340 points,否则需要滚动,破坏了快速交互的体验。如果你的内容确实需要更多空间,用按钮跳转到 App 内的完整视图。
UnionValues 是解决”一个 Query 返回多种 Entity 类型”的标准方案。如果你的搜索结果既有地标又有收藏夹,用 UnionValue 包装它们,别忘了为每种类型都实现 OpenIntent。
DeferredProperty 和 ComputedProperty 的选择标准:优先用 ComputedProperty(系统开销更低),只有当属性计算涉及网络请求或耗时 IO 时才用 DeferredProperty。
还有什么值得关注
- AppIntent 宏现在替代了旧的 AssistantIntent 宏,因为 schema 的范围已经扩展到 Visual Intelligence 等非 Siri 场景。
- Spotlight Mac 上执行 Action 时,IndexedEntity 的 indexingKey 参数可以把 Entity 属性映射到 Spotlight 的搜索 key,让用户输入”亚洲”就能过滤出亚洲的地标。
- Snippet 里的 contentTransition API 和交互式 Widget 用的是同一套——2023 年 Luca 的那个 Session 有详细介绍。
- onAppIntentExecution 视图修饰符可以把 UI 导航逻辑从 Intent 的 perform 方法里移出去,Intent 本身甚至可以完全去掉 perform。