iOS 27 中的 App Intents:后台执行、跨设备同步与 Spotlight 重建索引
App Intents 随 iOS 16 一同登场,作为 Apple 为 Shortcuts、Siri 和 Spotlight 打造的类型化、结构化动作API;iOS 17 将其扩展到由 App Intents 驱动的小组件;iOS 18 让它成为 Apple Intelligence 动作界面的契约;iOS 26 又把它推进到 Visual Intelligence 和交互式片段中。iOS 27 再次改变了这场押注的形态,而且这次的改变是机制层面的,而非表面修饰:一个 intent 现在可以突破 30 秒的后台限制继续运行,一个实体可以携带一个能在用户多台设备间往返而不丢失的身份,一个查询还能在系统要求时修复自己的 Spotlight 索引。iOS 27 带来的是新能力,而不是花架子。1
此前每一次发布都在拓宽谁能调用您的 intent。iOS 27 拓宽的是您的 intent 一旦被调用之后能做什么。过去,一个处理几千条记录的同步 intent 要和 30 秒的计时器赛跑,结果总是落败;如今它可以向系统申请更多运行时间,并在工作过程中报告进度。一个在 iPhone 上指代某物、在 Mac 上却指代另一物的实体,现在能在两端解析到同一个对象。本文将对照 Apple 文档逐一梳理 iOS 27 的这一界面,沿用本系列一贯的视角:一个已经搭载 App Intents 的应用,要获得每一项新能力需要补上什么。
TL;DR
LongRunningIntent将 intent 的后台运行时间延长至超过系统 30 秒的上限。您把工作包裹在performBackgroundTask(options:operation:)中并传入LongRunningTaskOptions;该协议细化自ProgressReportingIntent,因此报告进度是必备项,而非可选项。Live Activities 会自动渲染这一进度。234SyncableEntity声明某个AppEntity携带一个在用户各设备间保持一致的标识符,从而让系统能在 iPhone、Mac 和 Watch 上指代同一个对象(Siri 借此把一段对话从一台设备交接到另一台)。5IndexedEntityQuery为EntityQuery增加了 Spotlight 重建索引的支持,因此当系统发现您应用的索引出问题时,它可以要求您的查询重新捐献受影响的实体。6AppUnionValue和AppUnionValueCasesProviding(由@UnionValue宏生成)让单个参数能够接受若干种不同的实体类型,并配以合适的选择器 UI 和参数摘要。78OwnershipProvidingEntity、EntityOwnership和EntityCollection负责处理感知所有权的确认与批量操作的效率;RunSystemShortcutIntent和IntentExecutionTargets则负责小组件触发的系统动作以及由哪个进程来运行 intent。910111213
30 秒之墙:LongRunningIntent
后台执行限制一直是悬在 App Intent 能力之上的无声天花板。当系统在后台执行一个 intent 时(用户让 Siri 开始同步,随后锁屏并把手机揣进口袋),传统上只给大约 30 秒来完成。2 对于记录一杯水的摄入量,这绰绰有余。但对于同步一个资料库、运行端侧推理或处理一个大文件而言,30 秒是一把铡刀:系统在写入途中杀掉任务,用户拿到的是一个完成到一半的结果。
iOS 27 引入了 LongRunningIntent,这是一个 intent 采纳后即可向系统申请延长后台时间窗的协议。2 Apple 在文档中直接点明了适用场景:文件操作、数据同步、机器学习推理,以及对足够大的数据集进行的数据处理。这一声明在您动笔写下第一行代码之前,就把最重要的约束告诉了您:
protocol LongRunningIntent : ProgressReportingIntent
LongRunningIntent 细化自 ProgressReportingIntent。2 按设计,您无法在不报告进度的前提下采纳这个长时运行协议。延长的运行时间是系统有条件授予的特权,而条件就是您要持续告诉它进展到了哪一步。一旦停止报告,系统就可以撤销这一延长并提前结束任务。3
具体工作放在 performBackgroundTask(options:operation:) 内部:
@discardableResult
func performBackgroundTask<T>(
options: LongRunningTaskOptions = [],
operation: @escaping () async throws -> T
) async throws -> T
您在 intent 的 perform() 主体中调用该方法,并把耗时的代码放进 operation 闭包。在施加该限制的平台上,该方法会自动把您的运行时间延长至超过标准的 30 秒上限;您无需自己启动一个独立的后台任务,也无需自己管理 UIBackgroundTaskIdentifier。3 一个资料库同步 intent 看起来是这样的:
import AppIntents
struct SyncLibraryIntent: LongRunningIntent {
static var title: LocalizedStringResource = "Sync Library"
func perform() async throws -> some IntentResult {
try await performBackgroundTask(options: []) {
let records = try await server.fetchPendingRecords()
for (offset, record) in records.enumerated() {
try await store.apply(record)
progress.completedUnitCount = Int64(offset + 1)
progress.totalUnitCount = Int64(records.count)
}
return ()
}
return .result()
}
}
有两处值得解释,因为教程往往会略过它们。
progress 属性是契约,而不是遥测数据。 Apple 说得很明确:在您的操作运行期间,要定期更新来自 ProgressReportingIntent 遵从关系的 Progress;如果不更新,系统就可以取消这次运行时延长并提前结束您的任务。3 在普通 intent 上报告进度只是锦上添花。而在 LongRunningIntent 上,它是让延长保持存活的心跳。
LongRunningTaskOptions 用来声明资源需求。 这个选项值(一个默认为 [] 的 OptionSet 风格结构体)向系统说明该任务的额外资源需求,系统会把它纳入授予运行时间的考量。4 空集合是常见情形。只有当工作所需超出默认配置时,您才会动用显式选项。
突破 30 秒之外的回报是:Live Activities 会免费帮您渲染进度。文档指出,Live Activities 会利用它从 performBackgroundTask 自动接收到的信息来显示 intent 任务的进度,并根据您代码报告的数值绘制标题、副标题和进度条。3 一个由语音发起的长时同步,会以一条实时进度条的形式出现在锁定屏幕上,而您无需为它构建任何一个 Live Activity 视图。intent 负责报告,系统负责渲染。
LongRunningIntent 突破 30 秒限制继续运行,并在 Live Activity 上配有一个停止按钮,让用户可以随时取消。
在第 345 场会议中,Apple 用一个真实的失败案例来演示 LongRunningIntent——一个总在 30 秒窗口内夭折的照片上传——并展示系统如何管理后台任务的生命周期,同时以 Live Activity 的形式呈现进度和一个取消控件。14
一致的身份:SyncableEntity
一个 AppEntity 拥有一个 id。在单台设备上,这个标识符只需在应用内唯一即可。麻烦从用户拥有不止一台设备的那一刻开始——而在 Apple 的生态里,这恰恰是常态。用户在 iPhone 上和 Siri 讨论的那个“Project Atlas”,在他们于 Mac 上重新拾起对话时,必须能被识别为同一个“Project Atlas”。如果 iPhone 的本地标识符和 Mac 的不同,系统手里就有两个互不相关的对象,且无从将它们关联起来。
SyncableEntity 就是 iOS 27 给出的答案:5
protocol SyncableEntity : AppEntity
采纳它,即声明您实体的标识符在各设备间是相同的。这个协议的存在告诉系统:它可以在一台设备到另一台设备之间一致地指代您的实体。Apple 给出了具体的回报:Siri 利用这一能力把一段对话从一台设备转移到另一台。5
采纳成本完全取决于您的标识符从何而来。如果您的实体本就使用一个稳定的跨设备标识符(服务器签发的 UUID、iCloud 记录名),那么采纳 SyncableEntity 无需任何其他改动,因为您已经存储的那个值正是系统所需的值。5
import AppIntents
struct ProjectEntity: SyncableEntity {
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Project"
static var defaultQuery = ProjectQuery()
// A UUID issued by the backend and identical on every device.
var id: UUID
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name)")
}
@Property(title: "Name") var name: String
}
陷阱在于那种在每台设备上各自铸造一个全新本地标识符的应用(自增行 ID、按安装实例生成的 UUID)。这类标识符在本地唯一,跨设备却毫无意义。Apple 针对这种情况的建议是:采纳该协议,并把您的身份锚定在那个真正稳定的值上,这样系统才有一个持久的依托。5 同步一直是各团队反复用自己那套 iCloud 管线手工解决的难题。SyncableEntity 把跨设备身份的声明搬进了框架,让 Siri 和系统的其余部分能够据此行事。
自我修复的搜索:IndexedEntityQuery
iOS 16 让您能把 IndexedEntity 实例捐献给 Spotlight,从而使单个实体可被搜索。缺失的环节是修复。索引会漂移、会损坏,或在一次迁移后落后于现状,而在 iOS 27 之前,系统唯一的补救手段就是依赖您应用的 CSSearchableIndex 委托或它的 CSImportExtension。
IndexedEntityQuery 通过让系统请求您的查询来重建索引,补上了这一缺口:6
protocol IndexedEntityQuery : EntityQuery where Self.Entity : IndexedEntity
那条 where 子句就是前提条件:查询的实体必须遵从 IndexedEntity,因为重建索引只对那些您本就捐献给 Spotlight 的实体才有意义。6 当系统遇到某个应用索引的问题时,如果您的查询类型采纳了该协议,系统就会调用这个协议的方法;如果没有,Spotlight 会转而继续请求您的 CSSearchableIndex 对象(或您的 CSImportExtension,如果您是通过把实体与该类型关联来捐献的)来完成工作。6 您实现这些方法来获取被请求的实体,并通过您偏好的可搜索索引把它们重新捐献出去。
import AppIntents
import CoreSpotlight
struct PhotoQuery: IndexedEntityQuery {
func entities(for identifiers: [Photo.ID]) async throws -> [Photo] {
try await library.photos(matching: identifiers)
}
func suggestedEntities() async throws -> [Photo] {
try await library.recentPhotos(limit: 20)
}
// Called by the system during reindexing. Fetch the requested
// entities and donate them again to Spotlight.
func entities(matching string: String) async throws -> [Photo] {
try await library.photos(matchingText: string)
}
}
价值在于运维层面。一个正确处理了 IndexedEntityQuery 的应用,会参与到 Spotlight 的恢复循环中:系统注意到索引出错,应用便按需提供新鲜数据,而不是让用户悄无声息地丢失搜索结果,直到下一次完整的重新捐献。本系列的App Intents 基础篇讲过用裸 IndexedEntity 暴露条目以使其可被搜索;IndexedEntityQuery 则是叠加其上的维护层。
一个参数,多种类型:AppUnionValue
不少真实的 intent 会接受一个参数,而这个参数合理地可能是若干类型之一。“分享这个”,其中“这个”可能是一张照片、一份文档或一个链接。iOS 27 之前的变通办法都很难看:要么按类型拆成多个 intent,要么用一个字符串判别字段外加若干可选参数——而选择器 UI 根本无法把它们干净地呈现出来。
iOS 27 为类型化的联合参数加入了 AppUnionValue:7
protocol AppUnionValue : TypeDisplayRepresentable
一个遵从该协议的联合值可以作为带有丰富元数据的 Shortcuts 参数工作,因此系统能够跨各成员类型呈现一个合适的选择器和一份合理的参数摘要。7 这份遵从关系不用您手写。@UnionValue 宏会生成它,并且同一个宏还会生成一个嵌套的 Cases 枚举,该枚举遵从 AppUnionValueCasesProviding:78
protocol AppUnionValueCasesProviding : AppEnum
AppUnionValueCasesProviding 由宏所产出的 Cases 枚举自动遵从。8 它把这个 cases 枚举桥接回联合值类型,并通过其 AppEnum 遵从关系继承元数据——正是这一点赋予了每个 case 在选择器中的显示名称。8 实际写法是,您写出联合体并为它加上注解:
import AppIntents
@UnionValue
enum ShareTarget {
case photo(PhotoEntity)
case document(DocumentEntity)
case link(URL)
}
struct ShareIntent: AppIntent {
static var title: LocalizedStringResource = "Share Item"
@Parameter(title: "Item")
var target: ShareTarget
func perform() async throws -> some IntentResult {
// Switch over the concrete case and act accordingly.
return .result()
}
}
@UnionValue 宏负责处理 AppUnionValue 和 AppUnionValueCasesProviding 的遵从关系;如果您想要超出默认范围的自定义元数据,可以在一个扩展中实现这些协议要求。7 一个参数,三种有效类型,外加一个懂得如何把这三者都展示出来的选择器。
所有权与效率
iOS 27 中两个各自独立的关切被放进了同一节,因为二者都在保护系统不至于轻率地处置您的数据:感知所有权的确认,以及批量操作的效率。
确认破坏性动作:OwnershipProvidingEntity
当您的应用把实体传入 intent,又从结果中返回它们时,Apple Intelligence、Siri 和自定义快捷指令可以跨应用对这些实体进行操作。对于破坏性或敏感的动作(删除一个实体、更新一个共享的实体),您会希望确认提示携带恰当的上下文。OwnershipProvidingEntity 提供了它:9
protocol OwnershipProvidingEntity : AppEntity
让您的实体遵从它,当 intent 对共享的或可公开访问的实体进行操作时,系统就会弹出确认,并在对话框中附带恰当的上下文。9 所有权状态本身是一个 EntityOwnership 值,一个基于标志位的结构体,您可以指定单一状态,也可以用 OptionSet 组合多个状态:10
import AppIntents
struct AlbumEntity: OwnershipProvidingEntity {
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Album"
static var defaultQuery = AlbumQuery()
var id: UUID
var isSharedWithFamily: Bool
var isPublished: Bool
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name)")
}
@Property(title: "Name") var name: String
// Reflect how the user has shared this album so the system can
// calibrate its confirmation dialog.
var ownership: EntityOwnership {
var state: EntityOwnership = []
if isSharedWithFamily || isPublished {
state = .shared
}
return state
}
}
这一机制对拥有共享内容的应用最为重要:一个您已发布或已与家人共享的相册,应当触发比私有相册更谨慎的确认,而 OwnershipProvidingEntity 正是实体借以告诉系统孰是孰非的方式。9
批量而不吃内存:EntityCollection
解析实体并非没有代价。当一个 intent 接受成百上千个实体作为参数时,强迫系统在参数解析阶段把每一个标识符都解析成一个完整实例,会在一个糟糕的时刻耗费可观的时间和内存。EntityCollection 就是解法:11
struct EntityCollection<Entity> where Entity : AppEntity
该集合最初只为每个实体存储标识符,并在您需要时提供一个选项以稍后获取完整实例。11 当您手握大量标识符时,把它用作变量类型;当一个 intent 要在一个庞大集合上操作时,把它用作参数类型:
import AppIntents
struct DisableNotificationsIntent: AppIntent {
static var title: LocalizedStringResource = "Disable Notifications"
// Hundreds of conversations resolve lazily, not all at once.
@Parameter(title: "Conversations")
var conversations: EntityCollection<ConversationEntity>
func perform() async throws -> some IntentResult {
return .result()
}
}
对于一个持有成百上千个实体的参数而言,跳过逐标识符的解析,恰好在用户正等着这个动作启动的当口省下了时间和内存。11
intent 在哪里运行:RunSystemShortcutIntent 与 IntentExecutionTargets
iOS 27 还有两项较小的新增,把这一界面补充完整。RunSystemShortcutIntent 是一个仅供小组件使用的 intent,用于从小组件按钮启动另一个应用,或运行一个 App Shortcut、自定义快捷指令或系统动作:12
struct RunSystemShortcutIntent
您只能用它配合系统快捷指令的初始化器来初始化一个 Button 并把该按钮放进小组件;在该语境之外,它做不了任何有用的事。12 当用户配置小组件时,他们选择按钮的动作,而这个 intent 提供系统在配置 UI 中所需的元数据。它不会让您的小组件获得对某个快捷指令的动作、参数或实现的访问权。如果所选的快捷指令需要提示用户输入,系统可能会打开 Shortcuts 应用来执行它。12
IntentExecutionTargets 回答的是这样一个问题——一旦您通过一个 Swift 包或框架在应用、小组件扩展和 App Intents 扩展之间共享 intent 与实体,这个问题就会浮现:到底由哪个进程来运行这个 intent?13
struct IntentExecutionTargets
默认情况下,系统会使用任何可用的目标来执行一个 intent 或实体查询。13 您用 IntentExecutionTargets 来约束这一点。Apple 举的例子是一个浏览器:添加书签可以在应用不可见时进行,所以 App Intents 扩展就够了;但打开一个新标签页只有在应用可见时才有意义,这就需要应用自身的进程。13 您声明有效的目标,系统则遵守这一约束。
采纳路径
一个已经搭载 App Intents 的应用,可以增量地补上 iOS 27 的各项能力;它们当中没有一个会推翻核心模型。
- 找出您最慢的 intent。 任何在做文件 I/O、同步、端侧推理或大数据处理的 intent,都是
LongRunningIntent的候选。采纳该协议,把工作挪进performBackgroundTask(options:operation:),并自始至终报告progress。您将获得突破 30 秒的运行时间和免费的 Live Activities 进度。23 - 审查您的实体标识符。 如果它们已经跨设备稳定(服务器 UUID、iCloud 记录名),就让相关实体遵从
SyncableEntity并发布。如果它们是按设备生成的,先修好身份,再让其遵从。5 - 为那些实体属于
IndexedEntity的查询加上IndexedEntityQuery。 它是纯增量的:这些方法只在系统需要重建索引时才会被调用,而您的搜索结果会在索引漂移期间始终保持正确。6 - 用
@UnionValue收拢多类型参数。 凡是您曾用多个 intent 或一个判别字符串来伪造联合体的地方,这个宏都会给您一个干净的单一参数。7 - 用
OwnershipProvidingEntity标记共享实体,并把大集合参数切换为EntityCollection。 前者提升确认的安全性,后者提升解析的性能。911
常见问题
一个 LongRunningIntent 能在后台运行多久?
Apple 记录的是它抬升的下限,而非一个固定的上限。系统传统上给一个后台任务大约 30 秒来完成,而 LongRunningIntent(通过 performBackgroundTask(options:operation:))会在施加该限制的平台上自动把这个窗口延长至超过标准上限。23 这一延长是有条件的:您必须持续更新来自 ProgressReportingIntent 遵从关系的 Progress,一旦停止,系统就可以取消延长并提前结束您的任务。3 请把报告进度当作换取额外运行时间所付的代价。
使用 LongRunningIntent 必须报告进度吗?
是的。LongRunningIntent 被声明为 protocol LongRunningIntent : ProgressReportingIntent,因此采纳它就要求遵从 ProgressReportingIntent 及其 Progress。2 除了让编译器满意之外,定期的进度更新还能让后台运行时延长保持存活,并为 Live Activities 提供其从 performBackgroundTask 自动渲染的标题、副标题和进度条。3
SyncableEntity 在运行时究竟改变了什么?
它声明您实体的标识符在用户各设备间完全相同,从而让系统把该对象在任何地方都当作同一个实体,而不是按设备各自分开的对象。5 Apple 点名的具体能力是:Siri 可以把一段关于该实体的对话从一台设备转移到另一台。如果您的标识符本就跨设备稳定,采纳该协议无需任何其他改动;如果它们是按设备生成的,则先把身份重新锚定到一个稳定的值上。5
系统何时会调用 IndexedEntityQuery?
当它遇到您应用 Spotlight 索引的问题,且您的查询类型采纳了 IndexedEntityQuery(其实体遵从 IndexedEntity)时。6 系统会调用该协议的方法,让您获取受影响的实体并把它们重新捐献给 Spotlight。如果您的查询没有采纳该协议,Spotlight 会退回到请求您的 CSSearchableIndex 对象,或者您的 CSImportExtension(如果您是通过该类型捐献的)。6
为什么用 EntityCollection 而不是一个普通的实体数组?
EntityCollection<Entity> 一开始只为每个实体存储标识符,并在需要时再获取完整实例。11 作为 intent 参数,它能阻止系统在参数解析阶段强制把每一个标识符都解析成完整实例——对于一个持有成百上千个实体的参数,这在一个可能很关键的时刻省下了时间和内存。11 一个普通的 [Entity] 数组会急切地把一切都解析出来。
RunSystemShortcutIntent 能在小组件之外使用吗?
不能。它的唯一用途是配合系统快捷指令的初始化器来初始化一个 Button,以便放进小组件,在其他语境中它不提供任何功能。12 它为小组件的配置 UI 提供元数据,并代表用户所选的动作;它不会让您的小组件或应用获得对底层快捷指令的动作、参数或实现的访问权。12
完整的 Apple 生态系列:类型化的 App Intents;iOS 26 的新增;对照 MCP 工具的路由抉择;Foundation Models;全新的 Foundation Models 工具调用控制;运行时与工具链 LLM 之辨;三个界面;单一事实来源模式;与应用并存的 MCP 服务器;Live Activities;watchOS 运行时;SwiftUI 内部机制;SwiftData 架构纪律;Liquid Glass 模式;多平台交付;平台矩阵;Vision 框架;@Observable 内部机制;作为平台的无障碍。系列枢纽位于 Apple 生态系列。若想了解 iOS 与 AI 代理结合的更广背景,参见 iOS 代理开发指南。
参考资料
-
Apple Developer Documentation: App Intents. The framework reference covering
AppIntent,AppEntity, queries, parameters, and the iOS 27 additions. ↩ -
Apple Developer Documentation:
LongRunningIntent(iOS 27.0 beta). “An interface you use to extend the background execution time of an app intent that performs a long-running task.” Declared asprotocol LongRunningIntent : ProgressReportingIntent; the system traditionally gives background tasks up to 30 seconds. ↩↩↩↩↩↩↩ -
Apple Developer Documentation:
performBackgroundTask(options:operation:)(iOS 27.0 beta). Runs an operation in the background with extended time past the standard 30-second limit; requires regular progress updates or the system can cancel the extension; Live Activities renders the progress automatically. ↩↩↩↩↩↩↩↩↩ -
Apple Developer Documentation:
LongRunningTaskOptions(iOS 27.0 beta). Options for configuring long-running tasks; declares additional resource requirements, passed toperformBackgroundTask(options:operation:). ↩↩ -
Apple Developer Documentation:
SyncableEntity(iOS 27.0 beta). “An interface that indicates your entity has an identifier that’s consistent across devices.” Declared asprotocol SyncableEntity : AppEntity; Siri uses it to transfer a conversation between devices. ↩↩↩↩↩↩↩↩ -
Apple Developer Documentation:
IndexedEntityQuery(iOS 27.0 beta). “An interface that adds Spotlight reindexing support to your entity query.” Declared asprotocol IndexedEntityQuery : EntityQuery where Self.Entity : IndexedEntity. ↩↩↩↩↩↩↩ -
Apple Developer Documentation:
AppUnionValue(iOS 27.0 beta). “A protocol that provides nominal type identity and metadata for union values.” Declared asprotocol AppUnionValue : TypeDisplayRepresentable; conformance is generated by the@UnionValuemacro. ↩↩↩↩↩↩ -
Apple Developer Documentation:
AppUnionValueCasesProviding(iOS 27.0 beta). Declared asprotocol AppUnionValueCasesProviding : AppEnum; conformed to automatically by theCasesenum generated by the@UnionValuemacro. ↩↩↩↩ -
Apple Developer Documentation:
OwnershipProvidingEntity(iOS 27.0 beta). “A type that provides the system with ownership and sharing context for an app entity.” Declared asprotocol OwnershipProvidingEntity : AppEntity; prompts for confirmation on shared or publicly accessible entities. ↩↩↩↩↩ -
Apple Developer Documentation:
EntityOwnership(iOS 27.0 beta). “A type that represents the ownership and sharing characteristics of an app entity.” Declared asstruct EntityOwnership; flag-based, combinable with anOptionSet. ↩↩ -
Apple Developer Documentation:
EntityCollection(iOS 27.0 beta). “An array of entity identifiers that you use to improve the efficiency of operations involving large numbers of entities.” Declared asstruct EntityCollection<Entity> where Entity : AppEntity; stores identifiers initially and resolves full instances lazily. ↩↩↩↩↩↩↩ -
Apple Developer Documentation:
RunSystemShortcutIntent(iOS 27.0 beta). “An app intent you use in widgets to open another app or perform an App Shortcut, custom shortcut, or system action.” Declared asstruct RunSystemShortcutIntent; usable only to initialize a widgetButton. ↩↩↩↩↩↩ -
Apple Developer Documentation:
IntentExecutionTargets(iOS 27.0 beta). “A set of options that describes which process performs an intent or entity query.” Declared asstruct IntentExecutionTargets; constrains execution to the app, App Intents extension, or any available target. ↩↩↩↩ -
Apple, WWDC26 session 345, “Discover new capabilities in the App Intents framework.” developer.apple.com/videos/play/wwdc2026/345. Apple demonstrates
LongRunningIntentresolving a photo-upload intent that kept failing inside the 30-second limit, with the framework managing the background task lifecycle and surfacing progress plus a stop control as a Live Activity. ↩