探索 App Intents 框架的新功能
Discover new capabilities in the App Intents framework
2026年6月10日
一句话判断
这场 Session 最值得关注的一件事是:App Intents 终于通过 EntityCollection 和 LongRunningIntent 摘掉了“只能处理小数据的 30 秒玩具”的帽子,正式具备了接管生产环境重活的能力。
这场 Session 讲了什么
过去几年我们用 App Intents(应用意图)接入 Siri 和 Shortcuts(快捷指令)时,总觉得它在“过家家”。传几百个 Entity(实体)给 Intent 会卡死,稍微复杂点的网络请求超过 30 秒就被系统强杀,而且系统经常猜错你的 Intent 该在哪个进程跑。
这次 Apple 直接把这几个最恶心的痛点连根拔起。EntityCollection 让系统不再傻乎乎地在执行前解析所有 Entity 的全量属性,而是只传 ID,处理 1000 张照片的标签任务从卡死变成瞬秒。LongRunningIntent 则彻底打破了 30 秒的执行限制,配合 ExecutionTargets 让你能精准控制 Intent 到底是在主 App、Extension 还是 Widget 里跑。
对做效率工具、媒体处理或者重度依赖 Apple Intelligence 的 App 来说,这次更新是刚需。你终于可以把那些需要长时间后台处理、涉及大量本地数据的复杂操作,放心大胆地暴露给 Siri 和 Shortcuts 了。
值得深挖的点
EntityCollection:从“全量解析”到“按需传递”的性能救赎
以前 App Intents 的设计逻辑是“先解析,后执行”。当你传入一个 [PhotoEntity] 数组时,系统会在调用 perform() 之前,把每一张照片的所有属性(缩略图、元数据、路径)全部查出来塞进内存。传 10 张照片没感觉,传 1000 张直接 OOM(内存溢出)或者卡死 UI。这在真实业务里根本没法用。
EntityCollection 的设计非常克制且聪明。它把参数类型从实体数组变成了“实体标识符集合”。系统在分发 Intent 时只传递 ID,真正的 Entity 解析被推迟到了 perform() 内部,由开发者按需查询。这个 trade-off 极其合理:牺牲了一点点“无脑拿数据”的便利性,换来了数量级的性能提升。配合 SyncableEntity 提供的跨设备稳定 ID(Stable ID),你甚至可以在 iPhone 上发起 Siri 指令,在 Mac 上无缝继续处理这批数据,而不会因为本地 Core Data 的 Row ID 不同而找不到数据。
LongRunningIntent 与 ExecutionTargets:打破 30 秒魔咒与进程黑洞
App Intents 默认的 30 秒超时限制,逼着开发者把很多功能拆成“触发+轮询”的丑陋模式。LongRunningIntent 终于把后台任务生命周期管理权交还给了开发者。它不仅能跑超过 30 秒,还能原生绑定 Live Activity(实时活动)来展示进度,甚至支持后台 GPU 访问。
但光能跑得久还不够,跑错地方更致命。以前把 Intent 放在共享 Swift Package 里,系统靠启发式算法猜它该在哪个进程执行,经常导致 Widget 里的写操作去唤起主 App,或者主 App 里的读操作卡死 UI。ExecutionTargets 让你通过一个静态属性 allowedExecutionTargets 直接拍板:写操作必须在 .main,独立下载在 .appIntentsExtension,纯展示在 .widgetKitExtension。这种确定性的提升,对大型工程架构来说是救命的。
代码片段
1. 用 EntityCollection 处理大批量数据
场景:给 1000 张照片批量打标签,避免全量解析导致的卡顿。
struct TagPhotosIntent: AppIntent {
static let title: LocalizedStringResource = "批量标记旅行照片"
// 使用 EntityCollection 替代 [PhotoEntity],系统只传递 ID
@Parameter var photos: EntityCollection<PhotoEntity>
@Parameter var tag: String
func perform() async throws -> some IntentResult {
// 直接拿 identifiers 去数据库做批量更新,不触发全量 Entity 实例化
modelData.tagPhotos(ids: photos.identifiers, tag: tag)
return .result()
}
}
坑:photos.identifiers 拿到的是 ID 数组,如果你的后续逻辑强依赖 Entity 的某个非 ID 属性,还是得自己写 Query 去查,别指望系统帮你补齐。
2. 突破 30 秒限制的长时间后台任务
场景:上传大文件,结合进度汇报和优雅取消。
struct UploadPhotoIntent: LongRunningIntent, CancellableIntent {
static let title: LocalizedStringResource = "上传高清原图"
@Parameter var photo: IntentFile
func perform() async throws -> some IntentResult & ProvidesDialog {
// 包装在 performBackgroundTask 中,系统会接管后台生命周期并展示 Live Activity
let result = try await performBackgroundTask {
let chunks = calculateChunks(for: photo)
progress.totalUnitCount = Int64(chunks)
for chunk in 1...chunks {
// 响应 CancellableIntent 的取消事件
try Task.checkCancellation()
try await uploadChunk(chunk)
progress.completedUnitCount = Int64(chunk)
}
}
return .result(dialog: "上传完成")
}
// 实现 CancellableIntent 协议,处理清理工作
func onCancel() async {
await cleanupTempFiles()
}
}
坑:performBackgroundTask 闭包内抛出的错误不会自动触发 onCancel,你需要自己在闭包内处理异常,onCancel 只响应系统或用户主动发起的取消。
最佳实践
老项目迁移时,优先全局搜索 [YourEntity] 类型的 @Parameter。如果发现这个 Intent 经常处理超过 50 个实体的数组,立刻把类型换成 EntityCollection<YourEntity>,这能直接干掉潜在的 ANR(应用无响应)问题。
新项目设计 App Intents 架构时,把 Intent 定义和实现剥离。利用 ExecutionTargets 明确界定边界:涉及 Core Data 写入、Keychain 读取的 Intent 强制指定 .main;纯网络请求或数据转换的 Intent 扔到 .appIntentsExtension。不要依赖系统的默认启发式分配,那在复杂工程里就是个定时炸弹。
对于需要跨设备流转的 Entity,尽早引入 SyncableEntity。如果你的本地 ID 是自增整数或 Core Data 的 Object ID,用 SyncableEntityIdentifier 把本地 ID 和 CloudKit Record ID(或服务器 UUID)绑在一起,别等 Siri 跨设备找不到数据了再去改底层数据模型。
还有什么值得关注
ValueRepresentation:让 Entity 能直接导出为系统级结构化类型(如把自定义地点导出为PlaceDescriptor给 Maps 导航),不用再绕道Transferable传文件了。@UnionValue宏:一个参数现在能接受多种类型(比如 Widget 配置时既能选“相册”也能选“地点”),宏会自动生成 Shortcuts 和 Siri 需要的类型元数据。RelevantEntities:允许 App 主动向系统“塞”上下文相关的推荐内容(比如开始跑步时主动注册几个高 BPM 播放列表),不用再苦等用户交互来喂 Spotlight 索引。