Code-along: Make your app available to Siri
AI & Machine Learning 进阶 24m

跟随编程:让你的 App 支持 Siri

Code-along: Make your app available to Siri

2026年6月10日

在 Apple 官方观看视频

一句话判断

这场 Session 最值得关注的一件事是:App Schemas 彻底干掉了以前接入 Siri 时写 NLP 训练短语的玄学操作,把自然语言理解变成了强类型的”填空题”。

这场 Session 讲了什么

以前我们要让 Siri 听懂 App 里的业务,得在 SiriKit 里配一堆 Intent 定义文件,还得手写各种训练短语(Training Phrases)祈祷 NLP 模型能猜对用户的意思。现在 Apple Intelligence 接管了大脑,开发者只需要用 App Schemas(应用模式)把数据结构和动作”贴”到系统预定义的 Domain(比如 calendar domain)上。

Justin Kang 在 Session 里手把手演示了怎么把一个 SwiftUI 日历 App (CometCal) 接入 Siri。通过给 Entity 加上 IndexedEntity 协议,把数据喂给 Spotlight 做语义索引;用 TransientAppEntity 处理不需要独立搜索的子实体;最后用几个自带 Schema 的 Intent 搞定增删改查。这对所有做内容管理、日程、效率工具的开发者影响最大,以前需要一周的 Siri 适配工作,现在按着 Xcode 的代码补全(Snippets)一天就能搞定。

值得深挖的点

IndexedEntity 与 TransientAppEntity 的索引哲学

Apple 这次把 Spotlight 的语义索引和 App Intents 深度绑定了。当你让一个 Entity 遵循 IndexedEntity 时,你不仅是在告诉 Siri 这个数据长什么样,更是在把它塞进系统的语义搜索库里。但 Session 里有个非常精妙的设计选择:Attendee(参加者)被设计成了 TransientAppEntity(瞬态应用实体),而不是 IndexedEntity

为什么?因为如果同一个人参加了 10 个活动,把他作为独立实体索引 10 次,会在 Spotlight 里制造一堆重复的垃圾结果。参加者只有依附于 Event(事件)时才有意义。这种设计逼迫开发者在写代码前思考数据的”生命周期”和”搜索意图”。代价是,你不能直接通过 Siri 问”张三参加了什么”,只能问”周五的野餐有谁”。Apple 在”搜索精准度”和”数据扁平化”之间选择了前者,这提醒我们:不要把所有数据模型都无脑扔进 Spotlight,语义索引的边界在于”用户真的会独立搜索它吗”。

Update Intent 里的 valueState 陷阱

在做 UpdateEventIntent(更新事件意图)时,Justin 提到了一个极易踩坑的细节:IntentParametervalueState。当用户说”把会议地点改到会议室 A”,地点的 valueState 是 .set 且有值;当用户说”取消会议地点”,valueState 是 .set 但值为 nil;当用户根本没提地点,valueState 是 .unset

以前的 SiriKit 时代,区分”清空”和”不修改”简直是噩梦,往往需要加一个额外的 boolean flag 比如 clearLocation。现在 App Intents 直接在底层把状态机给你做好了。如果你在 perform() 方法里不看 valueState,直接把可选值覆盖进数据库,用户随口一句”把时间改到下午两点”,就会把你精心设置的地点和备注全部清空。这个设计把自然语言解析的模糊性,在 API 层面收敛成了严谨的三态逻辑。

代码片段

1. 将 Entity 捐赠给 Spotlight 语义索引

场景:在数据层增删改时,同步更新 Spotlight 索引,让 Siri 能搜到。

// 创建或更新日历时,同步到 Spotlight 语义索引
func createCalendar(_ model: CalendarModel) async throws {
    // 1. 保存到本地数据库 (SwiftData)
    let savedModel = try await saveToDatabase(model)
    
    // 2. 转换为 AppEntity
    let entity = savedModel.entity
    
    // 3. 捐赠给 Spotlight,Siri 现在能理解它了
    try await searchableIndex.indexAppEntities([entity])
}

// 删除时记得清理索引,否则 Siri 会搜到幽灵数据
func deleteCalendar(id: UUID) async throws {
    try await deleteFromDatabase(id: id)
    try await searchableIndex.deleteAppEntities(with: [UUID], type: CalendarEntity.self)
}

坑:indexAppEntities 是异步的且会抛错,千万别在 UI 线程同步调用,也别在数据库事务里直接 try!,索引失败不应该阻断本地数据的保存。

2. 处理 Update Intent 的三态逻辑

场景:更新事件时,精准区分用户是想”修改”、“清空”还是”保持原样”。

struct UpdateEventIntent: AppIntent {
    static var title: LocalizedStringResource = "Update Event"
    
    @Parameter(title: "Event")
    var event: EventEntity
    
    // 地点参数是可选的
    @Parameter(title: "Location")
    var location: String?
    
    func perform() async throws -> some IntentResult {
        // 获取参数的真实状态
        let locationState = $location.valueState
        
        switch locationState {
        case .set(let newValue):
            // 用户明确设置了新地点,或者明确要求清空 (newValue 为 nil)
            try await EventManager.updateLocation(for: event.id, to: newValue)
        case .unset:
            // 用户根本没提地点,保持原样,什么都不做
            break
        @unknown default:
            break
        }
        
        return .result()
    }
}

坑:千万别直接写 if let location { update(location) },这会导致用户无法通过 Siri 清空地点,也会在未提及地点时意外触发更新。

3. 让 Siri 拥有屏幕感知 (Onscreen Awareness)

场景:用户看着屏幕说”把这个事件推迟半小时”,Siri 需要知道”这个”指的是哪个。

struct EventDetailView: View {
    let event: EventEntity
    
    var body: some View {
        EventContent(event: event)
            // 绑定 UserActivity,并传入 Entity 的 ID
            // 当 Siri 听到 "this event" 时,会从这里提取上下文
            .userActivity("com.cometcal.viewEvent", element: event.id) { id, activity in
                activity.isEligibleForSearch = true
                activity.persistentIdentifier = id.uuidString
            }
    }
}

坑:element 传入的必须是实现了 AppEntity 的 ID 类型。如果传了普通的 String,Siri 的屏幕感知解析会直接失效。

最佳实践

  • 已有项目的迁移策略:别想着一次性把所有业务都接进 App Schemas。先挑出最核心的”名词”(如订单、文章、日程),给它们加上 IndexedEntity 并接入 Spotlight。跑通搜索后,再挑最高频的”动词”(如创建、打开)做 Intent。
  • 新项目的采用建议:在数据模型设计阶段就引入 @AppEntity。把 App Intents 的 Entity 作为你领域模型(Domain Model)的一等公民,而不是在 UI 层写完后再去硬写一层转换代码。利用 Xcode 的 Snippets(输入 calendar_ 等前缀)来生成 Schema,别自己手敲属性。
  • 实战避坑:屏幕感知(Onscreen awareness)非常依赖 View 的层级。如果你的详情页是被包在一个自定义的 NavigationStack 或者复杂的 TabView 里,.userActivity 可能会因为视图未完全渲染或失去焦点而丢失上下文。建议在真实的 Siri 语音环境下多测几次”这个”、“那个”的指代。

还有什么值得关注

  • 自定义 Snippet View:通过让 Intent 遵循 ShowsSnippetView,你可以用 SwiftUI 替换 Siri 默认的丑陋结果卡片,把 App 的品牌视觉(比如示例里的宇宙渐变背景)直接塞进 Siri 的对话流里。
  • OpenIntent 与导航绑定system.open Schema 配合 NavigationManager,让 Siri 不仅能搜到数据,还能直接 Deep Link 到 App 内的具体详情页,省去了手写一堆 URL Scheme 的麻烦。
  • AppIntentsTesting 框架:Apple 终于提供了专门测 Intent 的工具,不用再靠对着手机喊哑嗓子来测试 Siri 的意图解析了,写单元测试覆盖 valueState 逻辑成为可能。
App Intents App Schemas Siri Apple Intelligence Spotlight