WWDC Quick Look 💓 By SwiftGGTeam
Get to know App Intents

Get to know App Intents

观看原视频

Highlight

这场 Session 是 App Intents 框架的从零入门,从最基础的 intent、entity、query 三个核心概念讲起,一路延伸到 App Shortcut、Spotlight 索引、SwiftUI 导航集成等进阶用法。主讲人用一个地标浏览 app 做贯穿始终的 demo,逐步构建出一个能在 Spotlight、Siri、Action Button 中被调用的完整意图体系。


核心内容

很多 app 都有这样的痛点:用户想跳到某个具体的页面,必须先打开 app、再点几层菜单。Siri 喊一声、Spotlight 搜一下、Action Button 按一下就直达,听起来很美好,但中间隔着一整套 app 之外的系统能力。App Intents 就是 Apple 拿来填这条缝的框架。

主讲人 James 用一个地标 app 做演示。app 有三个 tab:Landmarks 列表、Map、Collections。他先写了 20 行代码,把”跳转到 Landmarks 页”包成 NavigateIntent。装上 app 之后,Shortcuts 里立刻能看到这个动作;再加一行 static let supportedModes: IntentModes = .foreground,运行时 app 会自动被拉到前台。然后他把跳转目标抽成 NavigationOption 这个 AppEnum,配合 @Parameter 让用户选择目的地,再补一个 AppShortcutsProvider,整套能力就在 Siri、Spotlight、Action Button 上同时点亮。

整个框架的设计哲学是把 app 的功能抽象成”动词”(intent)和”名词”(entity),然后通过 query 让系统理解你的数据。今年的更新集中在四件事:entity 属性支持 @ComputedProperty,从已有 model 延迟取值不必再拷贝;Spotlight 支持在 @Property 上直接标注 indexingKey;新增 TargetContentProvidingIntent 协议配合 onAppIntentExecution modifier 实现声明式导航;以及 App Intents 终于支持 Swift Package 和 static library 中定义类型,靠 AppIntentsPackage 注册跨 target 共享。


详细内容

App Intents 的最小单元是一个实现 AppIntent 协议的 struct,要求只有两件:一个 title、一个 perform()。下面是 demo 里最初的 NavigateIntent03:23):

struct NavigateIntent: AppIntent {
    static let title: LocalizedStringResource = "Navigate to Landmarks"

    static let supportedModes: IntentModes = .foreground

    @MainActor
    func perform() async throws -> some IntentResult {
        Navigator.shared.navigate(to: .landmarks)
        return .result()
    }
}

关键点:

  • title 必须是常量字符串,因为框架在编译期读取源码生成元数据,不能用计算属性。
  • supportedModes = .foreground 让 intent 执行前先把 app 拉到前台;默认值是后台执行。
  • @MainActor 保证 perform 在主线程跑,因为 navigation 必须在主线程上。
  • 返回 IntentResult 可以携带 dialog(Siri 朗读)、view snippet(系统弹窗中渲染)和 ReturnsValue(作为下一步 intent 的输入)。

光会跳一个固定页面太死板。第二步是把目标变成参数(05:38):

struct NavigateIntent: AppIntent {
    static let title: LocalizedStringResource = "Navigate to Section"

    static let supportedModes: IntentModes = .foreground

    static var parameterSummary: some ParameterSummary {
        Summary("Navigate to \(\.$navigationOption)")
    }

    @Parameter(
        title: "Section",
        requestValueDialog: "Which section?"
    )
    var navigationOption: NavigationOption

    @MainActor
    func perform() async throws -> some IntentResult {
        Navigator.shared.navigate(to: navigationOption)
        return .result()
    }
}

关键点:

  • @Parameter 把变量声明为 intent 输入;不加 Optional 表示必填,运行时会先要求用户提供值再调用 perform。
  • parameterSummarySummary("Navigate to \(\.$navigationOption)") 把动作和参数串成一句自然语言,Shortcuts 会把参数渲染成可点的 inline 控件。
  • requestValueDialog 是 Siri 询问用户时朗读的提示语。
  • 今年新规则:只要 intent 实现了完整的 parameter summary,就能在 macOS Spotlight 中直接运行(08:09)。

接下来要把 app 里真正动态的”名词”建模出来。Landmark 数量不固定,必须用 AppEntity。其中今年新增的 @ComputedProperty 让你不必把数据从 model 拷贝到 entity(11:02):

struct LandmarkEntity: AppEntity {
    var id: Int { landmark.id }

    @ComputedProperty
    var name: String { landmark.name }

    @ComputedProperty
    var description: String { landmark.description }

    let landmark: Landmark

    static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Landmark")

    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "\(name)")
    }

    static let defaultQuery = LandmarkEntityQuery()
}

关键点:

  • id 必须是持久化的、可用于数据库查找的标识符;系统会缓存这个 id,重启后仍能解析回原始 entity。
  • @ComputedProperty 是今年新加的,等价于 getter,Shortcuts 中读到这些字段时才去访问 model,避免重复存储。
  • defaultQuery 把 entity 与 EntityQuery 关联,系统通过这个 query 回答”这个 id 对应哪个 entity”等问题。

Query 是 entity 的”门牌号”。最基础的实现负责回答”What is the entity for this ID”(13:19):

struct LandmarkEntityQuery: EntityQuery {
    @Dependency var modelData: ModelData

    func entities(for identifiers: [LandmarkEntity.ID]) async throws -> [LandmarkEntity] {
        modelData
            .landmarks(for: identifiers)
            .map(LandmarkEntity.init)
    }
}

关键点:

  • @Dependency 把外部数据源注入 query;要在 app 启动早期通过 AppDependencyManager.shared.add { ModelData() } 注册。
  • entities(for:) 是所有 query 必须实现的方法,系统拿到 id 列表后通过它解析回 entity 实例。
  • 在此之上还可以扩展 EnumerableEntityQuery(返回全部)、EntityPropertyQuery(带谓词排序)、EntityStringQuery(按字符串匹配)三种查询协议,对应 Shortcuts 中的 Find、Filter、Search 行为。

最后一块是导航的声明式写法。今年新增的 TargetContentProvidingIntent 不再要求 perform 方法,导航逻辑直接挂在 SwiftUI view 上(18:17):

struct OpenLandmarkIntent: OpenIntent, TargetContentProvidingIntent {
    static let title: LocalizedStringResource = "Open Landmark"

    @Parameter(title: "Landmark", requestValueDialog: "Which landmark?")
    var target: LandmarkEntity
}

struct LandmarksNavigationStack: View {
    @State var path: [Landmark] = []

    var body: some View {
        NavigationStack(path: $path) {}
        .onAppIntentExecution(OpenLandmarkIntent.self) { intent in
            path.append(intent.target.landmark)
        }
    }
}

关键点:

  • OpenIntent 协议要求一个名为 target 的参数,并自动在 perform 之前把 app 拉到前台。
  • TargetContentProvidingIntent 让你跳过 perform 实现,把”拿到参数后做什么”挪到 view 层。
  • onAppIntentExecution(...) modifier 监听匹配的 intent,并在闭包里直接修改 SwiftUI navigation path,避免之前那种全局 Navigator 单例的胶水代码。

核心启发

  • 做什么:给现有 app 加一个最小化的 AppShortcut,把”打开常用页面”暴露给 Spotlight 和 Siri。

    • 为什么值得做:从 Session 给的成本看,这就是几十行 Swift;但用户体验立即升级——打开 app 不必先点首页再点 tab,Action Button 也能直接绑定。
    • 怎么开始:参照 03:23 的 NavigateIntent 写一个无参数版本,再在 AppShortcutsProvider 里挂一个短语,安装后立刻在 Shortcuts 里看到效果。
  • 做什么:把 app 里频繁出现的”领域对象”(订单、笔记、收藏夹、地点)建成 AppEntity 并实现 IndexedEntity

    • 为什么值得做:实现 IndexedEntity 后系统会代你把 entity 写进 Spotlight 索引,并支持语义搜索;用户在锁屏下拉就能搜到你的内容,竞争力直接拉到系统级。
    • 怎么开始:先用 @ComputedProperty 包裹现有 model 字段(避免数据双写),再给关键属性加 @Property(indexingKey:),最后调用 CSSearchableIndex 的 donate 方法。
  • 做什么:把 app 内部的导航逻辑从全局单例迁移到 TargetContentProvidingIntent + onAppIntentExecution

    • 为什么值得做:这种声明式写法让 deep link、Spotlight 点击、Siri 语音三条入口共享同一份导航代码,省掉手写 router;同时 SwiftUI 的状态可观察性也保留下来。
    • 怎么开始:先挑一个最常用的”打开详情页”场景,定义 OpenXxxIntent 实现两个协议;在 NavigationStack 上挂 onAppIntentExecution,闭包里 append path。
  • 做什么:给 entity 实现 Transferable,把图片、文档等代表性内容暴露给 Shortcuts 和其他 app。

    • 为什么值得做:用户能把你的 entity 直接喂进系统相册、消息、Mail 等动作里,多步 Shortcut 可以自动串起来;这是免费拿到的跨 app 互通能力。
    • 怎么开始:参考 16:33 的 DataRepresentation(exportedContentType: .image),把 entity 内部已有的图像数据声明出来即可。
  • 做什么:把 App Intents 类型放进 Swift Package,配合 AppIntentsPackage 协议跨 target 共享。

    • 为什么值得做:今年起 package 和 static library 都能定义 intent,方便把核心域模型和 UI 解耦;多 target 项目(主 app + extension + widget)可以共用一套 entity,避免重复声明。
    • 怎么开始:把 entity 文件搬到 SPM target,定义 struct XxxKitPackage: AppIntentsPackage {};在 app target 的 package 里通过 includedPackages 引入它。

关联 Session

评论

GitHub Issues · utterances