-
探索适用于 Siri 和 Apple 智能的 App Intents 高级功能
使用高阶 App Intents API,优化你的 App 与 Siri 的协作配合。了解一些实用技巧,让用户仅用语音就能完成更多任务、帮助 Apple 智能发现你的内容,并为屏幕感知提供情境信息,从而让 Siri 能够理解你 App 的行为。
章节
- 0:00 - Introduction
- 1:59 - Customize how Siri responds
- 4:20 - Visual responses
- 6:22 - Interaction donations
- 9:46 - Confirmations and entity ownership
- 11:59 - Semantic index with IndexedEntity
- 13:32 - Structured search with IntentValueQuery
- 15:27 - In-app search
- 16:22 - Onscreen awareness
- 20:51 - Leverage existing integrations
- 23:30 - Next steps
资源
- App Intents Testing
- Donating your app’s data and actions to the system
- Donations and discovery
- Making app entities available in Spotlight
- Making actions and content discoverable by Apple Intelligence
- Providing contextual cues to Apple Intelligence and Siri
- Apple Intelligence and Siri AI
相关视频
WWDC26
-
搜索此视频…
你好 我是Antonio Cancio App Intents团队的 软件工程师 让我们探索 Siri 和 Apple Intelligence 的高级 App Intents 功能
今天 你将学习这些技巧 让你的 App 在 Siri 和 Apple Intelligence 中的体验 超越基础 呈现精致 个性化且独具特色的效果 本次分享假设你已具备 App Intents 和 App Schemas 的基础知识 如果你还不熟悉 建议先观看这些专题 它们涵盖了基础内容
来看看今天的计划 首先 我们将探索如何让用户 通过 Siri 与你的 App 交互 变得更加直观自然 你将了解如何构建自定义响应 以匹配 App 的外观和体验 以及如何设置互动捐献 让 Apple Intelligence 带来更个性化的体验
然后 我们将讨论如何让 你的内容得到更广泛的呈现 从语义索引集成 到结构化搜索和 App 内搜索 以及屏幕感知 你将全面掌握 如何帮助 Siri 找到你的内容 并将屏幕上可见的内容 与其可理解的操作关联起来
最后 用户可以在系统任意位置 使用 Siri 我们将介绍如何添加实体注释 到现有的通知集成中 正在播放和闹钟 让用户无论在哪里遇到你的内容 都能对其采取行动 为了实际展示这些内容 我想分享我和同事 一直在构建的几个 App CosmoTunes 让我可以播放音乐 并用我喜欢的歌曲 创建闹钟和计时器
UnicornChat 是一款消息 App 让我与朋友保持联系 以及 CometCal 用于管理日历 嗯 有趣的是 App Intents 似乎特别钟爱天体符号 你可以从下方链接下载这些示例 并跟着操作 让我们从使用自定义响应 塑造 Siri 对话开始
Siri 负责繁重的工作 它理解自然语言 选择正确的操作 并给出有帮助的响应 App Intents 框架为你提供工具 来塑造 Siri 的行为 并优化它的响应方式 通过定制响应方式 你可以让 App 独特的 个性得以充分展现 让我用代码来说明这一点 在 CosmoTunes 中 .addToPlaylistIntent 让用户将歌曲添加到播放列表 首先 我希望 Siri 来处理响应 在 Intent 的 perform 方法中 我将歌曲添加到播放列表 并返回一个空的 IntentResult
这告诉 Siri 在 Intent 运行时 负责处理响应 尝试之后 我希望响应 更符合 App 的个性 我将歌曲称为曲目 将播放列表称为混音带 为了自定义 我将 perform 方法 标记为提供对话响应 通过添加 ProvidesDialog 协议 我还会通过传入 IntentDialog 来更改 IntentResult 其中包含完整字符串 和辅助字符串 Siri 可以在有界面时 显示辅助字符串 并在 AirPods 等纯语音设备上 朗读完整对话 因此 完整字符串应该能够 独立描述所发生的事情 这涵盖了 Intent 完成后的响应 但如果你想在 Intent 运行时 向用户提问呢 一个恰当的澄清问题 可以让用户完成 他们本想执行的操作 要在 Intent 结果之前提问 在 perform 方法中 使用对话请求
用户可以在 CosmoTunes 中创建计时器 以开始或停止音频播放 经过设定的时长后 这个 Intent 采用了一个 Schema 包含必需和可选参数 如果已有计时器在运行 我想请用户为新计时器命名 以避免混淆
我将为可选的 label 参数 请求一个值 当尚未提供时 如果你想让用户 从列表中选择 或请求确认 请查看示例 App 和文档 以了解其他类型的 对话请求 接下来 我希望 Siri 的视觉效果 与 App 的外观和体验相匹配 实体展示表示方式 以及 Intent 响应中的自定义视图 是以可视化方式呈现信息 的绝佳机会 并将 App 的标识 与对话一同展现
定义实体的 DisplayRepresentation 可以告知 Siri 显示你的内容时 实体应如何呈现和描述 实体展示表示方式 可用于响应 例如当实体被 创建或更新时 也用于请求用户在 相似实体中进行选择时 或回答关于 App 中 内容的问题时 Spotlight 和 Shortcuts 也可以使用它们 要定义基本的 DisplayRepresentation 请提供一个标题
你还可以通过提供副标题 和图片来使其更加丰富 我将提供与歌曲关联的图片 以便 Siri 在用户询问时展示 App 中的歌曲 实体 DisplayRepresentation 让你优化实体的视觉标识 在整个系统中 要为特定操作 定义视觉响应 你可以使用自定义视图片段 回到 AddToPlaylistIntent Siri 已经使用实体展示表示方式 自动响应
要使用自定义视图 我将 ShowsSnippetView 返回类型 添加到 perform 方法中 这让我可以返回带有 SwiftUI 视图的 IntentResult 比如我的 PlaylistSnippetView 以熟悉的颜色显示播放列表详情 在进行自定义时 测试你的 Intents 并决定 哪些自定义对你的 App 真正有意义 确保你的响应准确 并在所有平台上听起来自然 包括 AirPods 等纯语音设备 记得谨慎使用澄清问题 以避免产生摩擦 最后 使用自定义视觉效果 将 App 的标识融入 Siri 同时考虑它们在整个生态系统中 的适配表现
你的 Intent 响应通常出现在 Siri 交互结束时 但 Siri 甚至可以在调用 你的 Intent 之前提出额外问题 例如 如果我要求给某人发消息 Siri 可能会让我从 同名联系人列表中选择 因为系统不确定 我指的是哪一个 这就引出了互动捐献 这是一个你可以采用的 API 让 Apple Intelligence 变得更智能 以处理这类请求
好消息是 当用户通过 Siri 或 Shortcuts 与你的 App 互动时 系统已经知道这件事 但 Apple Intelligence 无法从用户在 你的 App 界面中的操作 中学习 除非得到你的帮助 这就是捐献的用武之地 每次捐献都是一个提示 表明用户在 App 界面中执行了特定操作 系统将这些存储为 符合 Schema 的 App Intents 存入临时记录 为 Siri 提供所需的上下文 以做出更智能的决策 记录包含随时间累积的 界面互动捐献 假设有人打开 UnicornChat 并从撰写视图发送消息 就在此时 App 将发送消息操作 捐献给系统 使用 SendMessageIntent Schema 在 App 中频繁向他们发消息后 最终 当用户说 "从主屏幕给联系人发消息" Siri 可能推断出 该联系人使用哪个 App 要采用互动捐献 用于消息发送 我首先查看了 UnicornChat 中的 ConversationView ConversationView 和 sendMessage Intent 都调用同一个辅助方法 来发送消息 这看起来是开始捐献 sendMessage Intent 的好地方 我将添加一个 donateIntent 参数 以便我知道辅助方法是从 Intent 还是从界面调用的 Apple Intelligence 已经从 Siri 互动中学习 所以我只需要捐献界面互动
然后 我将创建 Intent 填充其参数 和 Intent 结果 并通过 IntentDonationManager API 进行捐献 现在当用户打开 App 并发送消息时 系统可以学习他们何时 偏好使用 UnicornChat
除了学习偏好之外 互动捐献还让 Siri 了解 App 中的持续活动 这对用户可能通过 Siri 开始或停止的活动特别有用 在 Maps 领域的 App 中 用户可以通过 App 界面 开始一个 NavigationSession 这会捐献该互动 然后 用户上车后 请求 Siri 在途中添加一个停靠点 得益于互动捐献 Siri 可以知道 App 中 哪个 NavigationSession 正在活跃 并帮助用户完成请求 这个模式适用于 在 Maps 领域中开始 或停止 NavigationSessions 的 Intents 以及在 Clock 领域中 停止 开始 暂停或计圈秒表
你的互动捐献应该 准确代表 App 中用户的真实行为 如果你的 App 捐献过多 系统可能会忽略这些捐献 一旦 Siri 收集了 所有参数值 并准备好调用你的 App Intent 还有最后一步:确认
请用户确认操作是否正确 让他们了解情况并保护他们 免受意外副作用的影响 这是大型语言模型 已知的风险
这对可能对数据产生 重大副作用的 Intents 尤为重要 或对外部世界 这就是为什么 Siri 可以自动 确认这类 Intents 例如 当我说时它可以确认 "取消我下周在 CometCal 中的探险活动"
这对更新 App 内容的 Intents 更为重要 这些内容是用户 公开或与他人共享的 例如 Siri 可能不会确认 我更新个人日程 但当我要求更新 Crew Lunch 时 它可能会确认 因为我正在更新一个 有参与者的日程 默认情况下 Siri 假定你的实体 对用户是私有的 并且可能跳过对它们的确认
要告知 Siri 所有者已将 实体公开或与他人共享 将相关实体遵循新的 OwnershipProvidingEntity 协议 仅将协议添加到 用户可以在 App 中共享 或公开的实体 然后 提供所有权状态 保持所有权状态最新 每当系统从 App 请求实体时 这确保 Siri 在决定是否确认时 拥有必要信息
还记得我们之前自定义的 实体展示表示方式吗 还记得吗 Siri 也可以将它们用作 这些 Intent 确认的视觉效果 在适当时机给用户 确认操作的机会 可以建立用户对 App 与 Siri 体验的信任 要了解建立信任 和降低风险的其他方法 请查看"保护你的 App: 降低代理功能风险"
到目前为止 我已经定义了 这些 App 中的操作如何与 Siri 配合 为 Apple Intelligence 提供了 用户如何使用我的 App 的上下文 并帮助 Siri 保护用户 免受意外副作用的影响 接下来 让我们谈谈 Siri 首先是如何找到内容的 我想介绍三条路径 语义索引 结构化搜索 和 App 内搜索 CosmoTunes 中的播放列表 都在设备本地提供 为了帮助 Apple Intelligence 找到它们 我将采用 IndexedEntity 并在 Spotlight 中索引这些实体 我将使用 CSSearchableIndex 上的 .indexAppEntities 方法 这会填充 Spotlight 语义索引
现在我可以问 Siri "在 CosmoTunes 中播放我的 WWDC 播放列表"
我还可以在 Spotlight 搜索界面中搜索我的播放列表 根据 App Intents 领域 在 Spotlight 中索引实体 可提供语义搜索能力 这意味着 Apple Intelligence 和 Siri 可以基于含义来理解你的实体 而不仅仅是精确关键词
添加到索引是第一步 保持索引更新是帮助 Siri 找到你内容的关键 当用户在 App 中添加内容时 索引新实体 当关键属性更改时 更新现有条目 特别是那些用于 展示表示方式的属性
当用户删除内容时 也删除这些索引条目 Spotlight 可能需要 你的 App 重新索引其实体 你的 App 可以通过采用 新的 IndexedEntityQuery 来支持重新索引 在示例项目中 查看 IndexedEntityQuery 如果你的项目已经使用 Core Spotlight 级别的 API 支持重新索引 则不需要定义 IndexedEntityQuery
然而 如果你的内容数据集较大 你可能不会索引实体 存储在服务器上 或变化太频繁 无法提前索引 例如 我决定索引 App 的 所有播放列表 但不索引歌曲 为了仍然给用户提供 通过 Siri 播放歌曲的灵活性 我使用了 IntentValueQuery
如果你不提前索引所有实体 IntentValueQuery 很适合 这与 EntityQuery 非常相似 主要区别在于 你的 App 从系统接收 结构化搜索输入 并且你可以返回 多种实体类型 Siri 需要一个实体 用于 CosmoTunes 中 PlayAudioIntent 的 audioEntity 参数
为了找到实体 Siri 调用 IntentValueQuery 并传入 AudioSearch 查询将该搜索输入的 结构化属性 映射到 App 中的音频实体 在 IntentValueQuery 中 我实现了 values for 方法 来处理 AudioSearch 输入 并返回 AudioEntity AudioEntity 是一个 UnionValue 类型 包含歌曲和播放列表 AudioSearch 值 有一个 .criteria 属性 描述用户的查询 .searchQuery 情况包含 用户所说的相关部分 我用它来查找匹配的实体 App 还支持 未指定搜索 例如:"播放 CosmoTunes" 这没有具体说明 我想播放什么 在这种情况下 App 直接跳转 播放我之前喜欢的歌曲
还有一个 URL 情况 用于当用户引用 App 中的链接时 例如:"播放 Glow 发给我的那个播放列表"
查看文档以了解 AudioSearch 条件的完整集合
有时用户并不是 要求 Siri 采取行动 他们只是想找到某些内容 当我问:"在 CosmoTunes 中 显示跑步播放列表" Siri 可以显示 实体搜索结果列表 这是一个不错的默认效果 但我花了很多时间 精心打造 App 自己的搜索体验 我很想在那里展示这些结果
为此 我将采用 system .searchInApp Schema
iOS 17 中引入的 .system 搜索 Schema 现在命名为 .system.searchInApp 它是 System App Schema 领域的一部分 让用户通过 Siri 在你的 App 中搜索 无论你采用哪些其他领域 即使你不索引实体
Siri 使用它搜索的相同字符串 调用这个 Intent Intent 的 perform 方法 在 App 中找到并显示这些结果 Spotlight 和结构化搜索 让 Siri 能够推理你的内容 如果用户按名称 请求 Siri 播放 App 中的内容 就像在日常对话中一样 用户通常指向他们所见 这就是为什么我想让用户 与他们屏幕上正在看的 音频内容互动
屏幕感知是你的 App 将屏幕上可见内容 连接到系统理解的 结构化信息和操作的方式 然后 Siri 可以解析如 "播放第三首"或"那段对话"等引用 而无需用户 明确说出名称 当用户发起 Siri 请求时 Siri 对屏幕上的文本有所了解 但仅限于 像素中的确切内容 例如 Siri 无法对 显示的曲目采取行动 也可能无法告诉你 艺术家信息 因为屏幕上 当前没有显示艺术家
采用屏幕感知 API 为 Siri 提供屏幕上 实体的额外上下文 以及它们在屏幕上的位置 这种屏幕上下文意味着 Siri 可以回答关于 这些实体的详细问题 并对其采取行动 采用屏幕感知时 NSUserActivity 和 View Annotation API 是你应该开始的地方 使用 NSUserActivity 将 .userActivity 附加到视图 代表你的 主要屏幕内容 使用 View Entity 注释 当实体是屏幕上 众多项目之一时 将 .appEntityIdentifier 附加到 每个代表实体的视图 在 CosmoTunes 中 AlbumView 使用 View Entity 注释 因为专辑和 其中的曲目都可见 NowPlayingView 使用 NSUserActivity 因为屏幕专门用于 当前播放的项目 NSUserActivity 和 View Entity 注释 当屏幕上有少量实体时 已经足够 但还有另外两个 屏幕感知 API 第一个用于列表和集合 你同时显示多个实体的情况
CosmoTunes 中的曲目 在 App 的几个视图中以列表显示
集合注释帮我避免 了为每一行 附加注释的开销 取而代之 系统根据需要 懒加载地获取标识符 集合注释还让 Siri 能发现 已被选中 并滚动离开屏幕的实体 当视图离开视图层级时 逐行注释会立即消失 在 SwiftUI 中 在 List 上使用 .appEntityIdentifier(forSelectionType:) 修饰符 为每个项目的选择 ID 返回 EntityIdentifier
第二个 API 是 自定义画布视图注释 我构建了这个看起来像 钢琴卷帘的自定义画布视图 它展示了当前曲目中的音符 并带来 CosmoTunes 著名的 独特复古外观 我希望用户能够 对相关歌曲采取行动 无论何时使用 Siri 都可以看到这个画布 为了帮助系统理解 这个非标准子视图 我使用了自定义画布视图注释 如果你使用 SwiftUI 查看 我如何在 PianoRollView 中采用这个 在 CosmoTunes 示例代码中
UIKit 和 AppKit 也支持 所有屏幕感知 API 查看文档了解: AppEntityAnnotatable UICollectionViewAppIntentsDataSource 和 appEntityUIElementProvider 以及了解更多 关于这些实体注释如何帮助 在 UIKit App 中提供上下文菜单项 请查看"现代化你的 UIKit App"
采用屏幕感知后 App 的某些视图 同时显示多个实体
Siri 需要快速理解 屏幕上的实体是否 与请求相关 例如 用户要求 Siri 播放第三首 如果 Siri 无法足够快速地 理解我的屏幕实体 它可能会要求澄清 或播放完全不同的内容
发生这种情况时 用户可能会放弃请求 你之前自定义的实体展示表示方式 可以提供帮助
在 CosmoTunes 中 我在 播放列表实体查询上启用了 展示表示方式查询 通过实现 displayRepresentations 方法
现在 当 Siri 尝试 理解屏幕上的内容时 它可以只查询实体的 文本表示 跳过从数据库获取 完整内容的开销 屏幕感知为 Siri 提供了 额外上下文 当用户正在使用你的 App 时 除了界面之外 你的 App 已经 与系统其他部分紧密集成 为了给 Siri 更多上下文 你可以将实体连接到 你已经采用的集成中 比如用户通知 有了这个额外上下文 你的 App 实体就像通用语言 它们让 Siri 不仅理解 屏幕上的内容 还理解其他系统集成 以及时效性事件 如何与你的内容相关 我将向 App 已使用的 三个集成添加实体 UserNotifications NowPlaying 和 AlarmKit
完成后 我将能够说 "播放现场版" 让我轻松切换到 当前播放歌曲的不同版本
当 UnicornChat 通知 在 AirPods 上播报时
我可以说:"回复:好的 我会顺路去独角兽用品店 拿那些东西"
以及 Snooze 以推迟 CosmoTunes 中的闹钟 这三者都使用相同的模式 我们称之为实体注释
用实体注释通知 为 Siri 提供具体的实体上下文 在 AirPods 上播报通知时 收听播报的通知时 用户可能想对 背后的实体采取行动 比如回复消息 或完成提醒事项 为了给 Siri 额外上下文 说明哪个实体与 UnicornChat 通知相关 我将更新发送流程 导入 AppIntents 后 我将 持久化消息 EntityIdentifier 赋值给 UNMutableNotificationContent 上的 .appEntityIdentifiers 属性 注意 使用我描述的 三个实体注释 API 时 你不能使用 TransientAppEntity 瞬态实体是 临时模型对象 所以它们没有持久标识符 要在 CosmoTunes 中 向 NowPlaying 添加实体注释 我遵循了相同的模式 我已经通过 MusicContent 提供歌曲属性 在 App 的 MediaSessionRepresentable 遵循中 为了增强这个状态 我将获取现有的歌曲 艺术家和播放列表实体 并将它们添加到 appEntityIdentifiers 属性 按从最具体到 最不具体的顺序 这启用了上下文请求 例如"播放现场版"
使用 AlarmKit 我添加 一个 EntityIdentifier 到 AlarmConfiguration 上的 appEntityIdentifier 参数 创建闹钟或计时器时 这样 用户可以对 触发的闹钟和计时器采取行动 就这些 将你的实体连接到通知 NowPlaying 和闹钟
我们介绍了几种让 App 与 Siri 更好配合的高级方法 当你思考下一步时 一个很好的起点是 自定义你的实体展示表示方式 它们用于在整个系统中 显示你的实体 从那里 将你的实体 添加到语义索引 并保持索引最新 让 Siri 始终能找到你最新的内容
你还可以考虑通过 IntentValueQuery 和 App 内搜索 使你的实体通过 Siri 可访问 以及注释你的视图 活动 和你现有的系统 集成与实体 可以给 Apple Intelligence 提供更多上下文来使用
准备好后 考虑捐献界面互动 以帮助 Apple Intelligence 了解用户如何使用你的 App 带来更个性化的体验 要查看任何这些概念的应用 请查看示例项目 要亲自了解 如何采用 App Schemas 请查看"代码教程: 让你的 App 对 Siri 可用" 随着 27 个版本的发布 Apple Intelligence 正在改变 Siri 的能力 而 App Intents 将这种 变革性力量直接交到你手中 你现在拥有构建精致 卓越体验所需的一切 这些体验感觉像是 系统的自然延伸 我迫不及待地想看看你创造什么 下次再见
-
-
2:42 - Custom dialog response
@AppIntent(schema: .audio.addToPlaylist) struct AddToPlaylistIntent { func perform() async throws -> some IntentResult & ProvidesDialog { // Adds song to playlist and responds return .result( dialog: IntentDialog( full: """ Added \(song.title) to the \ \(playlist.title) mix tape. """, supporting: "Added" ) ) } } -
3:42 - Ask a clarifying question within an inten
@AppIntent(schema: .clock.createTimer) struct CreateTimerIntent { // MARK: Schema Parameters var duration: Duration var label: String? var isSleepTimer: Bool func perform() async throws -> some ReturnsValue<TimerEntity> { // Checks active timers and requests label parameter label = try await $label.requestValue( """ You already have a timer running. \ What should we call this one? """ ) return .result(value: timerEntity) } } -
4:26 - Enhanced DisplayRepresentation
// Enhanced DisplayRepresentation @AppEntity(schema: .audio.song) struct SongEntity { var displayRepresentation: DisplayRepresentation { DisplayRepresentation( title: "\(title)", subtitle: "\(artistName)", image: artworkImage ) } } -
5:05 - Return a custom snippet view
@AppIntent(schema: .audio.addToPlaylist) struct AddToPlaylistIntent { var audioEntity: AudioEntity var playlist: PlaylistEntity func perform() async throws -> some IntentResult & ProvidesDialog & ShowsSnippetView { // Adds to playlist and shows dialog and snippet let view = PlaylistSnippetView( playlist: updatedEntity, tracks: updated.tracks ) return .result(dialog: dialog, view: view) } } -
7:44 - Donate a UI interaction
@ModelActor actor ModelManager { func sendMessage(_ /* ... */, donateIntent: Bool = false) async throws -> [Message.ID] { // Donate intent with parameters and result so Siri can learn user preferences if donateIntent { let intent = SendMessageIntent() intent.destination = .recipients(conversation.recipients.map(\.entity)) let result = messages.map(\.entity) Task { try await IntentDonationManager.shared.donate( intent: intent, result: .result(value: result) ) } } } } -
10:03 - Declare entity ownership for confirmations
// Informs system if entity is public or shared with others @AppEntity(schema: .calendar.event) struct EventEntity: OwnershipProvidingEntity { var ownership: EntityOwnership { // isShared used to compute ownership state: .shared, .public, or .unknown attendees.isEmpty ? .unknown : .shared } } -
11:30 - Index entities with IndexedEntity
// Indexing IndexedEntity with CSSearchableIndex struct EntityIndexingHelper { // Indexes playlist entities func indexPlaylist(_ playlist: Playlist) async throws { let entity = PlaylistEntity(playlist: playlist) try await CSSearchableIndex(name: indexName) .indexAppEntities([entity]) } } -
13:38 - Structured search with IntentValueQuer
// Structured search of songs and playlists struct AudioIntentValueQuery: IntentValueQuery { // AudioSearch, IntentPerson, and other system types may be supported as input func values(for input: AudioSearch) async throws -> [AudioEntity] { switch input.criteria { case .searchQuery(let query): return try await searchResults(for: query) case .unspecified: return try await likedSongResults() // ... also a .url case } } } -
14:49 - Re-run Siri search in your app
// Intent that re-runs the Siri search in app @AppIntent(schema: .system.searchInApp) struct SearchAudioLibraryIntent { var criteria: StringSearchCriteria func perform() async throws -> some IntentResult { // Perform in-app search with Siri search string navigation.searchText = criteria.term navigation.selectedTab = .library return .result() } } -
16:27 - Onscreen awareness annotations
// (a) Single primary entity on screen — NSUserActivity struct NowPlayingView: View { @Environment(PlaybackController.self) private var playback var body: some View { VStack { // Player UI } .userActivity("cosmotunes.nowPlaying", isActive: playback.currentTrack) { activity in activity.title = playback.currentTrack?.title activity.appEntityIdentifier = EntityIdentifier( for: SongEntity.self, identifier: playback.currentTrack.id ) } } } // (b) One entity among many — View Entity annotation struct AlbumView: View { private var header: some View { VStack(alignment: .leading, spacing: 6) { // ... } .appEntityIdentifier( EntityIdentifier(for: AlbumEntity.self, identifier: session.id.uuidString) ) } } // (c) Lists and collections — Collection annotation struct PlaylistDetailView: View { var body: some View { List { ForEach(playlist.tracks) { track in PlaylistTrackRow(track: track) } } .appEntityIdentifier(forSelectionType: GeneratedTrack.ID.self) { trackID in EntityIdentifier(for: SongEntity.self, identifier: trackID) } } } -
17:23 - Component-based display representation query
// Component-based display representation queries extension PlaylistQuery { func displayRepresentations( for identifiers: [PlaylistEntity.ID], requestedComponents: DisplayRepresentation.Components = .text ) async throws -> [PlaylistEntity.ID: DisplayRepresentation] { let entities = try await model.playlistEntities(for: identifiers) // Fetch display representations for fetched entities var result: [PlaylistEntity.ID: DisplayRepresentation] = [:] for entity in entities { result[entity.id] = await entity.displayRepresentation(with: requestedComponents) } return result } } -
21:07 - Entity annotations on system integrations
// (a) User notifications import AppIntents import UserNotifications func scheduleNotification(message: Message, author: Contact, conversation: Conversation) { let content = UNMutableNotificationContent() content.title = author.name content.body = message.body // Annotate with entity identifier content.appEntityIdentifiers = [ EntityIdentifier(for: MessageEntity.self, identifier: message.id) ] // Schedule the notification } // (b) Now Playing — most specific to least specific import NowPlaying final class CosmoTunesMediaSession: MediaSessionRepresentable { var content: (any MediaContentRepresentable)? { var content = MusicContent(id: track.id.uuidString, songTitle: track.title /* ... */) content.appEntityIdentifiers = [ EntityIdentifier(for: SongEntity.self, identifier: track.id), EntityIdentifier(for: ArtistEntity.self, identifier: track.session.artistName), EntityIdentifier(for: PlaylistEntity.self, identifier: currentPlaylist.id), ] return content } } // (c) AlarmKit import AlarmKit func scheduleAlarm(_ alarm: Alarm) async throws { let configuration = AlarmManager.AlarmConfiguration<CosmoTunesAlarmMetadata>.alarm( schedule: schedule, attributes: attributes, appEntityIdentifier: EntityIdentifier(for: AlarmEntity.self, identifier: alarm.id), stopIntent: DismissAlarmIntent(), secondaryIntent: SnoozeAlarmIntent(), sound: sound ) // Schedule alarm }
-
-
- 0:00 - Introduction
Advanced App Intents techniques to make your app's Siri and Apple Intelligence experience feel polished and personal. Agenda: shape the Siri conversation, improve content discovery, and leverage existing integrations, demoed with the CosmoTunes, UnicornChat, and CometCal sample apps.
- 1:59 - Customize how Siri responds
Shape Siri's responses to match your app's voice: return an empty result to let Siri respond, or adopt ProvidesDialog and return an IntentDialog with full and supporting strings. Ask clarifying questions mid-intent with a dialog request (such as requesting an optional timer label).
- 4:20 - Visual responses
Give Siri your app's look: an entity's DisplayRepresentation (title, subtitle, image) is used across responses, disambiguation, Spotlight, and Shortcuts, while a custom SwiftUI snippet view (ShowsSnippetView) styles specific actions. Customize only where it helps, and account for voice-only devices.
- 6:22 - Interaction donations
System interactions are known automatically, but UI interactions aren't, so donate them via IntentDonationManager (using schema-conforming intents) so Apple Intelligence learns app preferences and stays aware of ongoing activities (such as Maps navigation or Clock stopwatches). Donate accurately; excessive donations are ignored.
- 9:46 - Confirmations and entity ownership
Siri auto-confirms intents with meaningful side effects, especially on shared or public content. Conform shareable entities to the new OwnershipProvidingEntity protocol and keep the ownership state current so Siri confirms appropriately, using your display representations as the confirmation visuals.
- 11:59 - Semantic index with IndexedEntity
Make local content discoverable: adopt IndexedEntity and index entities in Spotlight via indexAppEntities for meaning-based search. Keep the index fresh (add, update, delete), and support re-indexing with the new IndexedEntityQuery.
- 13:32 - Structured search with IntentValueQuery
For content too large, server-side, or fast-changing to index, use IntentValueQuery: the system passes a structured search input and you can return multiple entity types. CosmoTunes maps an AudioSearch (query, unspecified, or URL criteria) to a UnionValue of songs and playlists.
- 15:27 - In-app search
Adopt the system searchInApp schema (formerly system.search) so "Show me running playlists in CosmoTunes" re-runs Siri's search inside your own crafted search UI, regardless of which domains you adopt or whether you index entities.
- 16:22 - Onscreen awareness
Connect what's visible to entities so Siri resolves "play the third one." Start with NSUserActivity (single primary item) and View Entity annotations (appEntityIdentifier, one of many); scale up with collection annotations (forSelectionType:) and custom canvas annotations, all supported in UIKit and AppKit. Enable display-representation queries so Siri resolves on-screen entities fast.
- 20:51 - Leverage existing integrations
Attach entities to system integrations you already use: appEntityIdentifiers on UNMutableNotificationContent (reply to announced notifications), on Now Playing via MediaSessionRepresentable (for example "play the live version"), and appEntityIdentifier on AlarmKit's AlarmConfiguration ("snooze it"). Persistent entities only, no transient entities.
- 23:30 - Next steps
Start by customizing entity display representations, then index entities and keep the index current, add IntentValueQuery and in-app search, annotate views and existing integrations, and finally donate UI interactions. See the sample projects and "Code-along: Make your app available to Siri."