View in English

  • 打开菜单 关闭菜单
  • Apple Developer
搜索
关闭搜索
  • Apple Developer
  • 新闻
  • 探索
  • 设计
  • 开发
  • 分发
  • 支持
  • 账户
在“”范围内搜索。

快捷链接

5 快捷链接

视频

打开菜单 关闭菜单
  • 专题
  • 相关主题
  • 所有视频
  • 关于

更多视频

  • 简介
  • 概要
  • 转写文稿
  • 代码
  • 探索 App Intents 中的最新改进

    探索今年发布的 App Intents 框架新推出的所有增强功能。了解助力日常开发工作的诸多改进 (如延迟属性)、一系列新功能 (如交互式 App Intents 摘要片段、实体视图注释),以及如何整合视觉智能等。我们将一起探索让 App Intents 比以往更具表现力、更加流畅易用的各项更新。我们还将介绍今年 App Intents 激动人心的新增支持 (如支持“聚焦”和视觉智能),并讲解如何编写完美适配这些场景的 App Intents。

    章节

    • 0:00 - 简介
    • 0:55 - 交互式摘要卡片
    • 8:15 - 全新系统整合功能
    • 15:01 - 用户体验优化
    • 21:02 - 便捷功能 API

    资源

    • Accelerating app interactions with App Intents
    • Adopting App Intents to support system experiences
    • App intent domains
    • App Intents
    • App Shortcuts
    • Building a workout app for iPhone and iPad
    • Creating your first app intent
    • Integrating actions with Siri and Apple Intelligence
    • Making actions and content discoverable and widely available
    • PurchaseIntent
      • 高清视频
      • 标清视频

    相关视频

    WWDC25

    • 了解 App Intents
    • 使用 App Intents 针对“快捷指令”和“聚焦”进行开发
    • 设计交互式摘要卡片

    WWDC24

    • 利用 App Intents 为用户奉上 App 的核心功能
    • 利用 App Intents 设计提升系统体验
    • 带你的 App 登陆 Siri
    • App Intents 的新功能
  • 搜索此视频…

    大家好 我是 Jeff App Intents 团队的工程师 今天 我们将探索 App Intents 的 新进展

    App Intents 让你的 App 能够 将功能集成到用户的整个设备中 包括“快捷指令”、“聚焦”和 “Visual Intelligence”等

    如果你还不熟悉这个框架 我建议你先观看 “了解 App Intents”讲座 欢迎你随时回来 今天我们有很多内容要介绍 包括 你通过交互式摘要卡片可以打造的 新体验、 你的 App 在整个系统中利用 App Intents 的 更多方式、 你可以用来完善 App 用户体验 的更多功能 以及我们添加的、可增强开发者 体验的各种便捷 API 我们从交互式摘要卡片开始

    摘要卡片让你的 App 能够借助 App Intents 来显示定制视图 既可以用于请求确认 也可以用于显示结果 现在 你可以通过交互功能 让这些摘要卡片生动鲜活起来 比如 你可以创建一个园艺摘要卡片 建议在土壤太干燥时 打开洒水器 或者在将餐食订单发送到餐厅之前 先对订单进行配置 噢 还可以集成“实时活动” 等其他系统功能 这样在查看比分后 就可以立即 开始关注体育比赛 下面我将通过示例 App 来 快速演示一下 交互式摘要卡片 然后我们将探讨它的实现方式

    “TravelTracking”App 包含 世界各地的许多地标 我要轻点这个运行 App Intent 的 控件来查找最近的地标

    找到后 Intent 将展示一个 显示地标的摘要卡片 标题旁边有个爱心按钮 我来自多伦多 尼亚加拉大瀑布几乎就在我家后院 轻点爱心按钮就可将它添加到 我的收藏中

    摘要卡片将立即更新并显示新状态 这是采用新的 Snippet Intent 协议 构建的 这些意图是根据参数和 App 状态 来渲染视图的 你可以使用它们在完成某个操作后 显示结果或是请求用户进行确认 我们先来了解一下 结果摘要卡片的工作原理 App 中的任何意图都可以返回一个 填充了参数值的摘要卡片意图 作为结果的一部分 系统每次需要刷新摘要卡片时 都会使用这个方法 这时 系统将用你指定的值 填充摘要卡片意图参数 如果其中的某些参数是 App 实体 系统会从相应查询中获取这些参数值

    然后 系统会运行摘要卡片意图的 perform 方法 在那里 意图可以访问 参数和 App 状态 以便渲染视图 然后作为结果的一部分返回 视图可以将按钮或开关与 App 中的 任何 App Intents 相关联

    在这个示例中 爱心按钮运行的是 “更新个人收藏”意图 而“查找票证”按钮运行的是 “查找票证”意图 你不必仅为摘要卡片创建这些意图 实际上 无需修改即可重复使用 现有的摘要卡片

    轻点实体按钮后 系统会运行相应的意图 并等待它完成 这可确保个人收藏保持最新状态 完成后 系统将利用你之前返回的 参数化摘要卡片意图 来触发另一次更新 和之前一样 用来填充参数的 是你指定的值 再一次 系统将从查询中获取任何 App 实体 然后系统将运行 perform 方法 这让摘要卡片意图 有机会渲染更新的视图 在这个示例中 爱心图标现在已经填充 视图中的任何更改都将根据 SwiftUI 中的 contentTransition API 进行动画处理 这个循环会一直持续到 摘要卡片被忽略

    我们现在来实现返回摘要卡片意图 首先 我采用现有的意图 并将 ShowsSnippetIntent 添加到 返回类型中 然后用 result 方法的新参数提供 摘要卡片意图 这会告诉系统 每次使用摘要卡片 意图时 都用给定的地标来填充参数

    现在 我们来实现摘要卡片意图本身 首先 它需要符合 SnippetIntent 协议 然后 我将添加正确渲染视图所需的 变量 它们必须标记为参数 否则 系统不会填充它们 由于它是 App Intent 因此还可以 访问 AppDependencies 在 perform 方法返回类型中 添加 ShowsSnippetView 在主体中 使用方法来 获取生成视图所需的状态 然后使用 result 方法中的视图参数 来返回相应视图

    与 SwiftUI 视图类似 系统将创建 摘要卡片意图 并在生命周期内多次运行 因此 请确保它不会更改 App 状态 系统甚至可能会运行它更多次数 以便对设备更改做出响应 比如进入 深色模式 此外 请确保快速渲染视图 这样就不会感到反应迟钝

    而且由于参数值只指定一次 因此应只将这些参数值用于 每次都会查询的 App Entities 和 从不会改变的原始值 所有其他值都应该在 perform 方法中获取

    构建摘要卡片后 现在 我们来探讨一下 SwiftUI 视图 如何触发意图

    在视图主体中 LandmarkView 使用 Button 构造器 来关联它们对应的 App Intents 开关也有类似的 API

    这些 API 是通过 交互式小组件引入的 而 contentTransition 修饰符 可以用来自定动画

    如果想了解更多信息 请观看 Luca 2023 年的讲座 现在 我们将使用确认摘要卡片 来实现“查找票证”功能

    如果轻点“Find Tickets”按钮 它会询问我想搜索多少张票 我把数量增加到 4 因为我有朋友一起去 然后开始搜索 最终 它将通过结果摘要卡片来显示 所能找到的最便宜价格 让我们逐步了解一下这个过程 从结果片段代码开始 如果按钮或开关触发了 呈现自身片段代码的意图 它将替换原始意图 请记住 这仅在原始意图 显示结果时才有效 后续意图可以呈现两种摘要卡片的 任何一种 但在这里 我们使用请求确认 来显示配置视图 为此 只需调用 requestConfirmation 方法 你可以自定操作名称并提供驱动你 确认体验的摘要卡片意图

    如果在任何时候取消摘要卡片 这个方法将会抛出错误 不要尝试捕获它 直接让它终止 perform 方法

    然后与视图的每次交互都会 经历与结果摘要卡片相同的更新周期

    在这个示例中 摘要卡片意图始终显示 最新票数 这是怎么做到的呢? 嗯 因为我已经将搜索请求 建模为 AppEntity 系统在填充这个参数时 总是会 从查询中提取最新值 所以我只需将它传给视图 并自动获取最新值

    只要摘要卡片可见 系统就不会终止你的 App 所以 你可以随意在内存中保留状态 不需要将状态存储在数据库中

    完成所有交互后 当用户最终轻点 搜索操作时 将恢复执行原始的 App Intents

    有时 你可能希望摘要卡片 在任务执行过程中 因出现新状态而更新

    这种情况下 只需为 应该更新的摘要卡片意图 调用静态的 reload 方法即可

    请观看这个讲座 以进一步了解 如何设计最佳的交互式摘要卡片 摘要卡片只是起点 现在 我们来探索一下 App Intents 帮助你将 App 集成到 整个系统的更多方式 首先是图像搜索 这个功能让用户能够 直接从相机拍摄或截屏来执行搜索 这是 iOS 26 的新功能 你的 App 的搜索结果也可以显示出来 这里是个地标截屏 我将高亮显示它来执行图像搜索 系统将显示搜索面板 我可以选择“TravelTracking”App 来查看结果 然后当我轻点某个结果时 这个 App 就会打开相应的地标页面

    要支持图像搜索 需要实现一个 符合 IntentValueQuery 协议 的查询 它会接受 SemanticContentDescriptor 类型 作为输入 并返回 App Entities 数组 图像搜索将通过自己的显示模式 显示你的 AppEntities

    如果轻点某个结果 相应的 AppEntity 将会发送到 OpenIntent 以供 App 处理 这个 OpenIntent 必须存在 否则你的 App 将不会显示

    要实现查询 请先创建一个 符合 IntentValueQuery 协议 的结构体 Values 方法的输入必须是 SemanticContentDescriptor 它包含所选区域的像素 你可以使用 Video Toolbox 或 CoreImage 中的 API 来转换为熟悉的类型 比如 CGImage

    执行搜索后 应返回一个 由匹配的实体构成的数组 接下来 需要实现一个 符合 OpenIntent 协议的意图 来为轻点结果这项操作提供支持 目标参数的实体类型必须与结果相同 OpenIntents 不仅用于支持图像搜索 还可以在其他地方调用 比如在“聚焦”中 让用户能轻松导航到 App 中的实体

    用户要获得最佳体验 就需要能快速找到想要的东西 因此 返回几页结果有助于提高 在系统 UI 中显示完美匹配项的几率 你可以使用服务器来 查询较大的数据集 但不要搜索太久 否则 你会感到反应迟钝 最后 如果用户没有找到想要的内容 应让他们能够在你的 App 中 继续进行搜索 我们来把这个功能添加到 TravelTracking

    如果我在列表中没有看到想要的结果 可以轻点“更多结果”按钮 这样将 打开 App 并导航到搜索视图 要实现这一点 请使用新的 AppIntent 宏 来指定 semanticContentSearch 架构 这个新 API 取代了 AssistantIntent 宏 因为我们已经将架构扩展到 助理之外的功能 比如 VisualIntelligence 添加架构所需的 semanticContent 属性 宏会自动将它标记为意图参数

    在 perform 方法中 处理搜索元数据 然后导航到搜索视图 作为附加功能 我希望 TravelTracking 显示 包含匹配地标的集合 但是 我怎么能让一个查询同时返回 LandmarkEntity 和 和 CollectionEntity 呢? 答案是 UnionValues 首先声明一个 UnionValue 每个用例 代表查询可能返回的 一种实体类型 然后将结果类型更改为它们的数组 现在这两类实体都可以返回了 当然不要忘记为每类实体 实现 OpenIntent 如需进一步了解 UnionValues 请观看 2024 年由 Kenny 讲授的 App Intents 讲座

    App 内容可以在 App 之外搜索到 这很棒 但当 App 处于活跃状态时 借助 Apple 智能 你还能够做到更多 我们来谈谈屏幕实体 通过使用 NSUserActivities 你的 App 能够将实体 与屏幕内容相关联 这样 用户就可以向 ChatGPT 询问 与 App 上显示的内容有关的问题

    比如在“TravelTracking”App 中 我在注视尼亚加拉大瀑布时 可以问 Siri 这个地方是否靠近大海 Siri 会发现我指的是屏幕上的视图 于是会提议向 ChatGPT 发送截屏 但 LandmarkEntity 支持 PDF 所以我会选择“Full Content”选项 我可以快速预览一下 然后发送过去

    系统将显示回复 让我了解到 五大湖虽然很大 但不是海洋 为了将实体与视图关联 首先将 userActivity 修饰符添加 到 LandmarkDetailView 然后在更新闭包中 将实体标识符与活动相关联 接下来 我们需要支持将 LandmarkEntity 转换为 ChatGPT 可以理解的数据类型 比如 PDF

    为此 我们先让它符合 Transferable 协议 然后提供 PDF 数据表示形式 其他受支持的类型包括 纯文本和富文本 请查看这个 App Intents 文档 以便进一步了解屏幕实体

    说到向系统显示 App 的内容 我想简单谈谈“聚焦” 既然我们支持在 Mac 上直接从 “聚焦”运行操作 你可以用 App Intents 做一些事情 来实现最佳体验

    首先让 App 实体符合 IndexedEntity 并将它们提供给“聚焦” 这样一来 “聚焦”搜索就能提升 参数筛选体验 为了将实体属性与“聚焦”键相关联 现在你可以将新的 indexingKey 参数 用于 property 属性上 在这个示例中 我给 continent 添加了 customIndexingKey 这可以让用户搜索亚洲地标 只需在文本栏中键入“亚洲”即可 采用这个 API 的一个额外好处是 这可以让“快捷指令”App 为实体自动生成查找操作

    其次 用实体为屏幕内容添加注解 这样当这些实体 的视图可见时 将在建议中优先显示 第三 实现 PredictableIntent 这样系统便可以 根据用户之前的行为 所涉及的意图和参数来学习 并为你的意图提供相应建议 你甚至可以根据它们的值 提供定制描述

    如果你想进一步了解这个功能 请观看这个有关“快捷指令”和 “聚焦”的讲座 现在 你的 App 中已经具备 所有这些功能 接下来 我将向你展示一些优化 App 用户体验的新方法 首先介绍“撤销” 当用户知道自己能够改变主意时 他们会更愿意去尝试不同的事情 正因如此 让他们能够撤销在你的 App 中进行的操作就显得非常重要 借助新的 UndoableIntent 协议 你可以轻松让用户 使用他们已经知道的手势 来撤销你的 App Intents

    这是我在“TravelTracking”App 中的一些收藏集合 我将使用“通过键入使用 Siri” 来运行我的一个自定快捷指令 以删除 Sweet deserts 集合 确认删除后 它将从 App 中移除 如果我现在改变主意 就可以三指向左轻扫来触发撤销 并恢复这个集合

    DeleteCollectionIntent 通过首先 符合 UndoableIntent 协议来 参与撤销堆栈 这个协议提供可选的 undoManager 属性 你可以使用这个属性来注册撤消操作 setActionName 等其他操作也可用 通过这个属性 系统会为你的意图 赋予最相关的 撤销管理器 即使这些意图在 你的扩展中运行也不例外 这样可以确保 App 在 UI 和 App Intents 中的撤销操作保持同步 从而可以按照正确的顺序来撤消操作 我们可以提供删除的替代方案来 进一步完善这个意图

    我们可以使用新的多选 API 来提供 多个选项 供用户选择 我将再次运行删除集合的意图 这一次不再只是请我确认删除 它还提供了一个额外的归档选项

    嗯 这个集合可以让我回忆过去 我想还是把它归档吧 在 perform 方法中 显示多项选择 摘要卡片的方法是 调用 requestChoice 方法 并使用 between 参数提供选项数组 可以使用自定标题来创建选项 你还可以指定一种样式 告诉系统 如何渲染它

    然后可以使用对话框和自定 SwiftUI 视图来自定摘要卡片

    某个选项被选中后 会从 requestChoice 方法返回 但取消除外 这个选项会抛出错误 从而终止 perform 方法 不应捕获这个错误 如果请求被取消 意图应该立即停止

    接下来 使用 switch 语句 根据所选选项进行分支处理 可以使用创建好的原始选项作为 预期值 下面我们谈谈另一个改善的方面 受支持的模式让你的意图可以 更有效地控制 App 的前台显示 这让它们出现不同的行为 具体取决于用户与设备的交互方式 例如 如果我正在开车 我希望意图 仅通过语音给我提供信息 但如果我正在查看设备 它应该直接 将我带到 App 界面 因为那里可以向我展示更多信息 借助这个功能 我可以实现一个 可以同时完成这两件事的 App Intent 我来给你演示一下

    这是一个用于获取尼亚加拉大瀑布 人群状态的意图

    我将关闭“Open When Run”选项 这类似于无法将 App 置于前台的场景 比如戴着耳机使用 Siri 时 运行它时 只会显示一个对话框 Siri 也可以读取这个对话框

    但如果我关闭对话框并启用 “Open When Run”来运行它 系统将直接带我进入地标页面 其中显示了承载率 以及当前天气

    我们来把这个功能添加到意图中

    首先 我通过静态变量来 添加受支持的模式 如果我只将它设置为 background 就意味着告诉系统 这个 App 绝不会因 这一意图而置于前台显示 但如果可能的话 我还希望 意图能带我进入 App 所以我会添加前台模式 这个模式要求系统先启动 App 之后才运行意图

    然后我可以使用新的 currentMode 属性来检查 我们是否在前台 如果是 我们就可以相应地导航 嗯 等一下 为什么它说地标上没有人? 哦 因为关门了 我们来修改意图 让它在这种情况下 不打开 App 在 perform 方法中 我可以检查 地标是否处于开放状态 如果已关闭 就提前退出 这种情况下 我不希望系统在 运行这个意图之前 将 App 置于前台 因此我将前台模式修改为动态模式 受支持的模式包括 后台模式和三个前台模式 一种是 immediate 模式 它告诉系统 将 App 置于前台 然后运行意图 Dynamic 模式 它让意图能够决定 是否启动 App 最后是 deferred 模式 表示意图 最终将让 App 置于前台 只是不是立即这样做 GetCrowdStatus 可能无法启动 App 因此动态模式是完美的选择

    对于这两种模式 意图可以使用 continueInForeground 方法 以准确控制何时将 App 调至前台

    在确定地标处于开放状态后 我们可以在 systemContext 上使用 这个新属性 来检查是否可以将 App 置于前台 如果可以 我们将调用 continueInForeground 方法将它调至前台 通过将 alwaysConfirm 设为 false 如果过去几秒内有活动 系统将避免 提示 如果 App 启动成功 我们最终便可以 导航到人群状态视图 但如果启动请求被系统或用户拒绝 这个方法将抛出错误 你可以捕获错误并进行相应处理 结束之前 我想介绍一下我们为 简化你使用 App Intents 进行开发 所采取的一些方法

    如果 App Intents 想要执行 UI 导航 就需要拥有 驱动视图的所有 App 状态 的访问权限 这只有通过全局共享对象才能做到 比如 AppDependencies 或单一实例 但通过新的视图控制 API 你可以从 App Intents 中移除 UI 代码 让视图自行处理 让我们用它来重构“打开地标”意图 首先必须让意图符合 TargetContentProvidingIntent 协议 SwiftUI 视图中有个 path 属性 可用来以编程方式修改 NavigationStack 由于它被标记为状态 因此只能 从视图主体进行访问 这时 onAppIntentExecution 视图修饰符 就派上用场了 它采用你想要处理的 App Intent 类型 以及一个 action 闭包 其中包含传入的意图 在里面 我可以引用意图的参数 并相应地修改 UI 有了这段代码 我们可以从意图 本身移除 UI 代码 或者完全移除 perform 方法以及 我们不再需要的 依赖项 好棒啊

    系统很快就会运行你的 action 闭包 随后将 App 置于前台 因此 你只能从意图中读取参数值 不支持所有其他操作 例如请求值 如果多个视图具有相同的修饰符 它们都将运行 因此每个视图都可以 相应地响应 如果你的 App 支持多个场景或窗口 你可能需要控制 哪个场景或窗口运行特定的意图 比如 在“TravelTrackingApp”中 编辑某个集合时 我不希望它导航到与这个场景 相关的地标 这会非常干扰我的操作 所幸我可以通过 handlesExternalEvents API 来控制 提供意图的目标内容具有 contentidentifier 属性 默认为 persistentIdentifier 通常这是意图的结构体名称 你可以随时将这个值自定得更加具体

    我们可以将它与场景的 HandlesExternalEvents 修饰符 一起使用来设置激活条件 这个条件会指示系统使用场景 来执行 App Intents 如果场景还不存在 它会创建一个 确保数组中的标识符匹配 你要处理的意图的内容标识符属性 但如果激活条件是动态的 可以改为在视图上使用同一修饰符 在这里 我只允许场景 在我们没有编辑集合的情况下 处理 OpenLandmarkIntent 要进一步了解 SwiftUI 场景和 激活条件 可以观看这两个讲座 对于 UIKit 你可以让意图符合 UISceneAppIntent 协议 这个协议让意图能够 通过这些成员来访问 UIScene 或者 你的场景委托在符合 AppIntentSceneDelegate 协议后 可以对意图的执行进行响应 最后 你还可以使用激活条件来决定 哪个场景将处理 App Intent 下一个改进是引入了新的 ComputedProperty 宏 它能让你避免在 AppEntities 上 存储值 这是我的 SettingsEntity 它从 UserDefaults 拷贝了 defaultPlace 我想避免在这样的结构体上 存储重复值 相反 这个值应该直接从 真实数据源推导得出 有了新的 ComputedProperty 宏 我可以直接从 getter 访问 UserDefaults 来实现这一点

    我们还添加了新的 DeferredProperty 宏来降低 实例化 AppEntity 的成本 LandmarkEntity 中有一个 crowdStatus 属性 它的值来自 网络服务器 因此获取它需要较高的开销 只有当系统明确请求它时 我才想 获取它

    通过将这个属性标记为 DeferredProperty 我可以提供一个异步 getter 在内部 我可以调用进行网络调用的 方法 这个异步 getter 被调用的 唯一条件是 “快捷指令”等系统功能发出请求 而不是在 LandmarkEntity 被创建和传递时

    这里列出了三种属性类型的 一些主要区别 总之 选择 ComputedProperty 而不 是 Deferred 的原因是系统开销较低 仅当这个属性的计算成本过高时 才回退到 Deferred 最后但同样重要的是 你现在可以将 App Intents 放入 Swift 软件包中

    以前 我们允许通过框架和动态库来 打包 AppIntents 代码 现在你可以将它们放入 Swift 软件 包和静态库中 要进一步了解如何使用 AppIntensePackage 协议 请观看“了解 App Intents”讲座 希望大家和我一起学习这些功能时 能有所收获 接下来 请尝试使用交互式摘要卡片 来看看它们能为你的意图做些什么 将实体与屏幕内容相关联 以便系统可以自动 向用户推荐这些实体 提供更多选项 供用户选择 使用多项选择 API 支持多种模式 以确保无论你的意图 以哪种方式运行 都能提供最佳体验 最后 要更深入了解代码 可以前往开发者网站查看 我们的示例 App 非常高兴终于可以和大家 分享这些内容 我迫不及待地想看看大家 会想出哪些创意十足的点子

    今天的讲座就到这里 谢谢观看!

    • 4:08 - Returning a Snippet Intent

      import AppIntents
      import SwiftUI
      
      struct ClosestLandmarkIntent: AppIntent {
          static let title: LocalizedStringResource = "Find Closest Landmark"
      
          @Dependency var modelData: ModelData
      
          func perform() async throws -> some ReturnsValue<LandmarkEntity> & ShowsSnippetIntent & ProvidesDialog {
              let landmark = await self.findClosestLandmark()
      
              return .result(
                  value: landmark,
                  dialog: IntentDialog(
                      full: "The closest landmark is \(landmark.name).",
                      supporting: "\(landmark.name) is located in \(landmark.continent)."
                  ),
                  snippetIntent: LandmarkSnippetIntent(landmark: landmark)
              )
          }
      }
    • 4:31 - Building a SnippetIntent

      struct LandmarkSnippetIntent: SnippetIntent {
          static let title: LocalizedStringResource = "Landmark Snippet"
      
          @Parameter var landmark: LandmarkEntity
          @Dependency var modelData: ModelData
      
          func perform() async throws -> some IntentResult & ShowsSnippetView {
              let isFavorite = await modelData.isFavorite(landmark)
      
              return .result(
                  view: LandmarkView(landmark: landmark, isFavorite: isFavorite)
              )
          }
      }
    • 5:45 - Associate intents with buttons

      struct LandmarkView: View {
          let landmark: LandmarkEntity
          let isFavorite: Bool
      
          var body: some View {
              // ...
              Button(intent: UpdateFavoritesIntent(landmark: landmark, isFavorite: !isFavorite)) { /* ... */ }
      
              Button(intent: FindTicketsIntent(landmark: landmark)) { /* ... */ }
              // ...
          }
      }
    • 6:53 - Request confirmation snippet

      struct FindTicketsIntent: AppIntent {
      
          func perform() async throws -> some IntentResult & ShowsSnippetIntent {
              let searchRequest = await searchEngine.createRequest(landmarkEntity: landmark)
      
              // Present a snippet that allows people to change
              // the number of tickets.
              try await requestConfirmation(
                  actionName: .search,
                  snippetIntent: TicketRequestSnippetIntent(searchRequest: searchRequest)
              )
      
              // Resume searching...
          }
      }
    • 7:24 - Using Entities as parameters

      struct TicketRequestSnippetIntent: SnippetIntent {
          static let title: LocalizedStringResource = "Ticket Request Snippet"
      
          @Parameter var searchRequest: SearchRequestEntity
      
          func perform() async throws -> some IntentResult & ShowsSnippetView {
              let view = TicketRequestView(searchRequest: searchRequest)
      
              return .result(view: view)
          }
      }
    • 8:01 - Updating a snippet

      func performRequest(request: SearchRequestEntity) async throws {
          // Set to pending status...
         
          TicketResultSnippetIntent.reload()
      
          // Kick off search...
      
          TicketResultSnippetIntent.reload()
      }
    • 9:24 - Responding to Image Search

      struct LandmarkIntentValueQuery: IntentValueQuery {
      
          @Dependency var modelData: ModelData
      
          func values(for input: SemanticContentDescriptor) async throws -> [LandmarkEntity] {
              guard let pixelBuffer: CVReadOnlyPixelBuffer = input.pixelBuffer else {
                  return []
              }
      
              let landmarks = try await modelData.searchLandmarks(matching: pixelBuffer)
      
              return landmarks
          }
      }
    • 9:51 - Support opening an entity

      struct OpenLandmarkIntent: OpenIntent {
          static var title: LocalizedStringResource = "Open Landmark"
      
          @Parameter(title: "Landmark")
          var target: LandmarkEntity
      
          func perform() async throws -> some IntentResult {
              /// ...
          }
      }
    • 10:53 - Show search results in app

      @AppIntent(schema: .visualIntelligence.semanticContentSearch)
      struct ShowSearchResultsIntent {
          var semanticContent: SemanticContentDescriptor
      
          @Dependency var navigator: Navigator
      
          func perform() async throws -> some IntentResult {
              await navigator.showImageSearch(semanticContent.pixelBuffer)
      
              return .result()
          }
      
          // ...
      }
    • 11:40 - Returning multiple entity types

      @UnionValue
      enum VisualSearchResult {
          case landmark(LandmarkEntity)
          case collection(CollectionEntity)
      }a
      
      struct LandmarkIntentValueQuery: IntentValueQuery {
          func values(for input: SemanticContentDescriptor) async throws -> [VisualSearchResult] {
              // ...
          }
      }
      
      struct OpenLandmarkIntent: OpenIntent { /* ... */ }
      struct OpenCollectionIntent: OpenIntent { /* ... */ }
    • 13:00 - Associating a view with an AppEntity

      struct LandmarkDetailView: View {
      
          let landmark: LandmarkEntity
      
          var body: some View {
              Group{ /* ... */ }
              .userActivity("com.landmarks.ViewingLandmark") { activity in
                  activity.title = "Viewing \(landmark.name)"
                  activity.appEntityIdentifier = EntityIdentifier(for: landmark)
              }
          }
      }
    • 13:21 - Converting AppEntity to PDF

      import CoreTransferable
      import PDFKit
      
      extension LandmarkEntity: Transferable {
          static var transferRepresentation: some TransferRepresentation {
              DataRepresentation(exportedContentType: .pdf) {landmark in
                  // Create PDF data...
                  return data
              }
          }
      }
    • 14:05 - Associating properties with Spotlight keys

      struct LandmarkEntity: IndexedEntity {
      
          // ...
      
          @Property(indexingKey: \.displayName)
          var name: String
      
          @Property(customIndexingKey: /* ... */)
          var continent: String
      
          // ...
      }
    • 15:49 - Making intents undoable

      struct DeleteCollectionIntent: UndoableIntent {
          // ...
      
          func perform() async throws -> some IntentResult {
      
              // Confirm deletion...
      
              await undoManager?.registerUndo(withTarget: modelData) {modelData in
                  // Restore collection...
              }
              await undoManager?.setActionName("Delete \(collection.name)")
      
             // Delete collection...
          }
      }
    • 16:52 - Multiple choice

      struct DeleteCollectionIntent: UndoableIntent {
          func perform() async throws -> some IntentResult & ReturnsValue<CollectionEntity?> {
              let archive = Option(title: "Archive", style: .default)
              let delete = Option(title: "Delete", style: .destructive)
      
              let resultChoice = try await requestChoice(
                  between: [.cancel, archive, delete],
                  dialog: "Do you want to archive or delete \(collection.name)?",
                  view: collectionSnippetView(collection)
              )
      
              switch resultChoice {
              case archive: // Archive collection...
              case delete: // Delete collection...
              default: // Do nothing...
              }
          }
          // ...
      }
    • 18:47 - Supported modes

      struct GetCrowdStatusIntent: AppIntent {
      
          static let supportedModes: IntentModes = [.background, .foreground]
      
          func perform() async throws -> some ReturnsValue<Int> & ProvidesDialog {
              if systemContext.currentMode == .foreground {
                  await navigator.navigateToCrowdStatus(landmark)
              }
      
              // Retrieve status and return dialog...
          }
      }
    • 19:30 - Supported modes

      struct GetCrowdStatusIntent: AppIntent {
          static let supportedModes: IntentModes = [.background, .foreground(.dynamic)]
      
          func perform() async throws -> some ReturnsValue<Int> & ProvidesDialog {
              guard await modelData.isOpen(landmark) else { /* Exit early... */ }
      
              if systemContext.currentMode.canContinueInForeground {
                  do {
                      try await continueInForeground(alwaysConfirm: false)
                      await navigator.navigateToCrowdStatus(landmark)
                  } catch {
                      // Open app denied.
                  }
              }
      
              // Retrieve status and return dialog...
          }
      }
    • 21:30 - View Control

      extension OpenLandmarkIntent: TargetContentProvidingIntent {}
      
      struct LandmarksNavigationStack: View {
      
          @State var path: [Landmark] = []
      
          var body: some View {
              NavigationStack(path: $path) { /* ... */ }
              .onAppIntentExecution(OpenLandmarkIntent.self) { intent in
                  self.path.append(intent.landmark)
              }
          }
      }
    • 23:13 - Scene activation condition

      @main
      struct AppIntentsTravelTrackerApp: App {
          var body: some Scene {
              WindowGroup { /* ... */ }
      
              WindowGroup { /* ... */ }
              .handlesExternalEvents(matching: [
                  OpenLandmarkIntent.persistentIdentifier
              ])
          }
      }
    • 23:33 - View activation condition

      struct LandmarksNavigationStack: View {
          var body: some View {
              NavigationStack(path: $path) { /* ... */ }
              .handlesExternalEvents(
                  preferring: [],
                  allowing: !isEditing ? [OpenLandmarkIntent.persistentIdentifier] : []
              )
          }
      }
    • 24:23 - Computed property

      struct SettingsEntity: UniqueAppEntity {
      
          @ComputedProperty
          var defaultPlace: PlaceDescriptor {
              UserDefaults.standard.defaultPlace
          }
      
          init() {
          }
      }
    • 24:48 - Deferred property

      struct LandmarkEntity: IndexedEntity {
          // ...
      
          @DeferredProperty
          var crowdStatus: Int {
              get async throws {
                  await modelData.getCrowdStatus(self)
              }
          }
      
          // ...
      }
    • 25:50 - AppIntentsPackage

      // Framework or dynamic library
      public struct LandmarksKitPackage: AppIntentsPackage { }
      
      // App target
      struct LandmarksPackage: AppIntentsPackage {
          static var includedPackages: [any AppIntentsPackage.Type] {
              [LandmarksKitPackage.self]
          }
      }
    • 0:00 - 简介
    • App Intents 是一个框架,它使 App 能够跨设备集成各种功能,例如快捷指令、聚焦和视觉智能。了解新的交互式摘要片段、系统级 App 集成、优化的用户体验,以及面向开发者的便捷 API。

    • 0:55 - 交互式摘要卡片
    • 借助最新更新,你现在可以使用交互式摘要片段来增强自己的 App。这些摘要片段是动态视图,可根据 App Intents 显示定制信息。例如,园艺 App 可以建议打开洒水器,订餐 App 可以允许用户在下单前配置订单。 “TravelTracking”示例 App 就是一个很好的例子。当用户搜索最近的地标时,会出现一个交互式摘要片段,其中包含地标名称和一个心形按钮。轻点心形按钮,用户可以收藏这一地标,摘要片段也会立即更新以反映新状态,用户在整个过程中都无需离开当前视图。 这种交互性是通过新的“SnippetIntent”协议实现的。你可以创建在操作后显示信息的结果摘要片段,这些摘要片段可以包含触发其他 App Intents 的按钮或开关。例如,在“TravelTracking”App 中,轻点心形按钮将会运行“更新个人收藏”意图,而且你可以添加“查找门票”按钮来启动另一个意图。 当用户与这些按钮交互时,系统会运行相应的意图,摘要片段也会相应更新。每当摘要片段刷新时,系统都会从查询中提取 App 实体,从而确保数据始终保持最新。这个过程是一个无缝衔接的动态过程,可提供流畅的用户体验。 你还可以创建确认摘要片段,要求用户在继续操作之前提供更多信息。例如,在“TravelTracking”App 中,当用户轻点“查找门票”时,系统会显示一个确认摘要片段,询问用户要搜索多少张门票。然后,用户可以与这个片段交互,系统则会根据用户的输入实时更新视图。

    • 8:15 - 全新系统整合功能
    • iOS 26 中的 App Intents 增强了系统集成,支持直接通过相机拍摄或截屏进行图像搜索。App 现在可以在系统搜索面板中显示搜索结果。为了支持这项功能,请实现符合“IntentValueQuery”协议的查询,通过处理“SemanticContentDescriptor”数据来返回 App 实体数组。 当用户轻点某个结果时,会触发相应的“OpenIntent”,从而在 App 中跳转到相关页面。“OpenIntents”并不仅限于图像搜索,你还可以在“聚焦”中使用它们。请考虑优化搜索性能,以便返回多页结果并允许用户在 App 中继续搜索。 除了图像搜索之外,App Intents 还支持屏幕实体,允许用户与 Siri 和 ChatGPT 就 App 中的可见内容进行互动。你可以将实体与视图相关联,使其符合“Transferable”协议,并支持各种数据类型 (例如 PDF、纯文本和富文本)。 若要增强“聚焦”搜索,请使 App 实体符合“IndexedEntity”,将它们提供给“聚焦”,并为屏幕上的实体内容添加注释。通过实现“PredictableIntent”,系统能够学习用户行为并提供个性化建议。

    • 15:01 - 用户体验优化
    • App 开发平台中的新功能可通过改进的撤消功能、多选选项和支持的模式来增强用户体验。 “UndoableIntent”协议允许用户使用熟悉的手势撤销通过 App Intents 执行的操作,从而为实验提供安全保障。你可以通过使用户的意图符合该协议并在撤消管理器中注册撤消操作来实现这一点。 借助多选 API,你可以为用户提供多个意图选项,而不仅仅是二进制确认。你还可以使用对话框和自定 SwiftUI 视图来进行自定。 “支持的模式”使意图能够更好地控制 App 的前台显示。这一功能允许意图根据用户与设备交互的方式做出不同的行为。例如,意图可以在用户开车时提供纯语音信息,但在用户注视设备时则直接转到 App。你可以为他们的意图指定支持的模式,并使用“currentMode”属性来检查哪个模式处于有效状态。

    • 21:02 - 便捷功能 API
    • 借助 SwiftUI 中全新的视图控制 API,你可以移除用户界面代码并让视图直接处理导航,从而重构 App Intents。请使用“onAppIntentExecution”视图修饰符 (它使视图能够响应特定的 App Intents),并相应地修改用户界面。 在即将在前台显示 App 之前,系统会运行操作闭包,并且多个视图可以对同一意图做出响应。你可以使用“handlesExternalEvents”API 来控制由哪个场景处理意图,从而确保导航符合上下文。 此外,“ComputedProperty”和“DeferredProperty”等新的宏可以优化“AppEntities”,从而降低存储和实例化成本。 App Intents 现在还可以打包到 Swift 软件包中,提供了更高的灵活性和可重用性。

Developer Footer

  • 视频
  • WWDC25
  • 探索 App Intents 中的最新改进
  • 打开菜单 关闭菜单
    • iOS
    • iPadOS
    • macOS
    • Apple tvOS
    • visionOS
    • watchOS
    打开菜单 关闭菜单
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    打开菜单 关闭菜单
    • 辅助功能
    • 配件
    • App 扩展
    • App Store
    • 音频与视频 (英文)
    • 增强现实
    • 设计
    • 分发
    • 教育
    • 字体 (英文)
    • 游戏
    • 健康与健身
    • App 内购买项目
    • 本地化
    • 地图与位置
    • 机器学习与 AI
    • 开源资源 (英文)
    • 安全性
    • Safari 浏览器与网页 (英文)
    打开菜单 关闭菜单
    • 完整文档 (英文)
    • 部分主题文档 (简体中文)
    • 教程
    • 下载 (英文)
    • 论坛 (英文)
    • 视频
    打开菜单 关闭菜单
    • 支持文档
    • 联系我们
    • 错误报告
    • 系统状态 (英文)
    打开菜单 关闭菜单
    • Apple 开发者
    • App Store Connect
    • 证书、标识符和描述文件 (英文)
    • 反馈助理
    打开菜单 关闭菜单
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program (英文)
    • News Partner Program (英文)
    • Video Partner Program (英文)
    • 安全赏金计划 (英文)
    • Security Research Device Program (英文)
    打开菜单 关闭菜单
    • 与 Apple 会面交流
    • Apple Developer Center
    • App Store 大奖 (英文)
    • Apple 设计大奖
    • Apple Developer Academies (英文)
    • WWDC
    获取 Apple Developer App。
    版权所有 © 2025 Apple Inc. 保留所有权利。
    使用条款 隐私政策 协议和准则