axiom-app-intents-ref by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-app-intents-ref将应用功能暴露给 Siri、Apple Intelligence、快捷指令、聚焦搜索和其他系统体验的 App Intents 框架综合指南。用现代 Swift 优先的 API 取代了旧的 SiriKit 自定义意图。
核心原则 App Intents 让你的应用操作在 Apple 生态系统中可被发现。设计良好的意图在 Siri 对话、快捷指令自动化和聚焦搜索中感觉自然。
App Intents 与以下系统集成:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
允许用户在 Visual Intelligence 相机中圈选对象,并查看来自你应用的匹配结果:
@UnionValue
enum VisualSearchResult {
case landmark(LandmarkEntity)
case collection(CollectionEntity)
}
struct LandmarkIntentValueQuery: IntentValueQuery {
func values(for input: SemanticContentDescriptor) async throws -> [VisualSearchResult] {
// 将视觉输入与应用实体匹配
}
}
// 每种实体类型都需要一个 OpenIntent
struct OpenLandmarkIntent: OpenIntent { /* ... */ }
struct OpenCollectionIntent: OpenIntent { /* ... */ }
将应用实体与可见内容关联,以便用户可以询问 Siri 或 ChatGPT 关于屏幕上显示的内容:
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)
}
}
}
1. AppIntent — 带有参数的可执行操作
struct OrderSoupIntent: AppIntent {
static var title: LocalizedStringResource = "Order Soup"
static var description: IntentDescription = "Orders soup from the restaurant"
@Parameter(title: "Soup")
var soup: SoupEntity
@Parameter(title: "Quantity")
var quantity: Int?
func perform() async throws -> some IntentResult {
guard let quantity = quantity, quantity < 10 else {
throw $quantity.needsValue("Please specify how many soups")
}
try await OrderService.shared.order(soup: soup, quantity: quantity)
return .result()
}
}
2. AppEntity — 用户交互的对象
struct SoupEntity: AppEntity {
var id: String
var name: String
var price: Decimal
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Soup"
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name)", subtitle: "$\(price)")
}
static var defaultQuery = SoupQuery()
}
3. AppEnum — 参数的枚举类型
enum SoupSize: String, AppEnum {
case small
case medium
case large
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Size"
static var caseDisplayRepresentations: [SoupSize: DisplayRepresentation] = [
.small: "Small (8 oz)",
.medium: "Medium (12 oz)",
.large: "Large (16 oz)"
]
}
struct SendMessageIntent: AppIntent {
// 必需:简短的动词-名词短语
static var title: LocalizedStringResource = "Send Message"
// 必需:目的说明
static var description: IntentDescription = "Sends a message to a contact"
// 可选:在快捷指令/聚焦搜索中发现
static var isDiscoverable: Bool = true
// 可选:运行时启动应用
static var openAppWhenRun: Bool = false
// 可选:身份验证要求
static var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication
}
struct BookAppointmentIntent: AppIntent {
// 必需参数(非可选)
@Parameter(title: "Service")
var service: ServiceEntity
// 可选参数
@Parameter(title: "Preferred Date")
var preferredDate: Date?
// 带有 requestValueDialog 用于消除歧义的参数
@Parameter(title: "Location",
requestValueDialog: "Which location would you like to visit?")
var location: LocationEntity
// 带有默认值的参数
@Parameter(title: "Duration")
var duration: Int = 60
}
struct OrderIntent: AppIntent {
@Parameter(title: "Item")
var item: MenuItem
@Parameter(title: "Quantity")
var quantity: Int
static var parameterSummary: some ParameterSummary {
Summary("Order \(\.$quantity) \(\.$item)") {
\.$quantity
\.$item
}
}
}
// Siri: "Order 2 lattes"
func perform() async throws -> some IntentResult {
// 1. 验证参数
guard quantity > 0 && quantity < 100 else {
throw ValidationError.invalidQuantity
}
// 2. 执行操作
let order = try await orderService.placeOrder(
item: item,
quantity: quantity
)
// 3. 捐赠以供学习(可选)
await donation()
// 4. 返回结果
return .result(
value: order,
dialog: "Your order for \(quantity) \(item.name) has been placed"
)
}
enum OrderError: Error, CustomLocalizedStringResourceConvertible {
case outOfStock(itemName: String)
case paymentFailed
case networkError
var localizedStringResource: LocalizedStringResource {
switch self {
case .outOfStock(let name):
return "Sorry, \(name) is out of stock"
case .paymentFailed:
return "Payment failed. Please check your payment method"
case .networkError:
return "Network error. Please try again"
}
}
}
func perform() async throws -> some IntentResult {
if !item.isInStock {
throw OrderError.outOfStock(itemName: item.name)
}
// ...
}
struct BookEntity: AppEntity {
// 必需:唯一、持久的标识符
var id: UUID
// 应用数据属性
var title: String
var author: String
var coverImageURL: URL?
// 必需:类型显示名称
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Book"
// 必需:实例显示
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: "by \(author)",
image: coverImageURL.map { .init(url: $0) }
)
}
// 必需:用于解析的查询
static var defaultQuery = BookQuery()
}
struct TaskEntity: AppEntity {
var id: UUID
@Property(title: "Title")
var title: String
@Property(title: "Due Date")
var dueDate: Date?
@Property(title: "Priority")
var priority: TaskPriority
@Property(title: "Completed")
var isCompleted: Bool
// 暴露给系统用于过滤/排序的属性
}
直接从事实源读取的计算属性(无存储值):
struct SettingsEntity: UniqueAppEntity {
@ComputedProperty
var defaultPlace: PlaceDescriptor {
UserDefaults.standard.defaultPlace
}
init() { }
}
计算成本高的属性,仅在明确请求时获取:
struct LandmarkEntity: IndexedEntity {
@DeferredProperty
var crowdStatus: Int {
get async throws {
await modelData.getCrowdStatus(self)
}
}
}
struct BookQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [BookEntity] {
// 通过 ID 获取实体
return try await BookService.shared.fetchBooks(ids: identifiers)
}
func suggestedEntities() async throws -> [BookEntity] {
// 提供建议(最近、收藏等)
return try await BookService.shared.recentBooks(limit: 10)
}
}
// 可选:启用基于字符串的搜索
extension BookQuery: EntityStringQuery {
func entities(matching string: String) async throws -> [BookEntity] {
return try await BookService.shared.searchBooks(query: string)
}
}
// 不要让你的模型遵循 AppEntity
struct Book: AppEntity { // 不好 - 将模型与意图耦合
var id: UUID
var title: String
// ...
}
// 你的核心模型
struct Book {
var id: UUID
var title: String
var isbn: String
var pages: Int
// ... 许多内部属性
}
// 用于意图的独立实体
struct BookEntity: AppEntity {
var id: UUID
var title: String
var author: String
// 从模型转换
init(from book: Book) {
self.id = book.id
self.title = book.title
self.author = book.author.name
}
}
struct ViewAccountIntent: AppIntent {
// 无需身份验证
static var authenticationPolicy: IntentAuthenticationPolicy = .alwaysAllowed
}
struct TransferMoneyIntent: AppIntent {
// 要求用户登录
static var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication
}
struct UnlockVaultIntent: AppIntent {
// 要求设备解锁(面容 ID/触控 ID/密码)
static var authenticationPolicy: IntentAuthenticationPolicy = .requiresLocalDeviceAuthentication
}
使用 supportedModes 对执行上下文进行细粒度控制,而不是布尔值 openAppWhenRun:
struct GetCrowdStatusIntent: AppIntent {
static let supportedModes: IntentModes = [.background, .foreground(.dynamic)]
func perform() async throws -> some ReturnsValue<Int> & ProvidesDialog {
guard await modelData.isOpen(landmark) else {
return .result(value: 0, dialog: "The landmark is currently closed.")
}
if systemContext.currentMode.canContinueInForeground {
do {
try await continueInForeground(alwaysConfirm: false)
await navigator.navigateToCrowdStatus(landmark)
} catch {
// 打开应用被拒绝
}
}
let status = await modelData.getCrowdStatus(landmark)
return .result(value: status, dialog: "Current crowd level: \(status)")
}
}
| 模式 | 行为 |
|---|---|
.background | 完全在后台执行 |
.foreground(.immediate) | 在 perform() 运行前将应用置于前台 |
.foreground(.dynamic) | 可以在执行期间请求前台 |
.foreground(.deferred) | 最初在后台,完成前在前台 |
| 组合 | 使用时机 |
|---|---|
[.background, .foreground] | 前台默认,后台回退 |
[.background, .foreground(.dynamic)] | 后台默认,可以请求前台 |
[.background, .foreground(.deferred)] | 最初在后台,请求时保证前台 |
在使用 .foreground(.dynamic) 时,在运行时请求前台转换:
// 正常转换
try await continueInForeground(alwaysConfirm: false)
// 错误后转换
throw needsToContinueInForegroundError(
IntentDialog("Need to open app to complete this action"),
alwaysConfirm: true
)
struct QuickToggleIntent: AppIntent {
static var openAppWhenRun: Bool = false // 在后台运行
func perform() async throws -> some IntentResult {
// 无需打开应用即可执行
await SettingsService.shared.toggle(setting: .darkMode)
return .result()
}
}
struct EditDocumentIntent: AppIntent {
@Parameter(title: "Document")
var document: DocumentEntity
func perform() async throws -> some IntentResult {
// 打开应用以在 UI 中继续
return .result(opensIntent: OpenDocumentIntent(document: document))
}
}
struct OpenDocumentIntent: AppIntent {
static var openAppWhenRun: Bool = true
@Parameter(title: "Document")
var document: DocumentEntity
func perform() async throws -> some IntentResult {
// 应用现在在前台,可以安全地更新 UI
await MainActor.run {
DocumentCoordinator.shared.open(document: document)
}
return .result()
}
}
struct DeleteTaskIntent: AppIntent {
@Parameter(title: "Task")
var task: TaskEntity
func perform() async throws -> some IntentResult {
// 在破坏性操作前请求确认
try await requestConfirmation(
result: .result(dialog: "Are you sure you want to delete '\(task.title)'?"),
confirmationActionName: .init(stringLiteral: "Delete")
)
// 用户确认,继续
try await TaskService.shared.delete(task: task)
return .result(dialog: "Task deleted")
}
}
使用结构化选项请求用户输入:
let options = [
IntentChoiceOption(title: "Option 1", subtitle: "Description 1"),
IntentChoiceOption(title: "Option 2", subtitle: "Description 2"),
IntentChoiceOption.cancel(title: "Not now")
]
let choice = try await requestChoice(
between: options,
dialog: IntentDialog("Please select an option")
)
switch choice.id {
case options[0].id: // 选项 1 被选中
case options[1].id: // 选项 2 被选中
default: // 已取消
}
返回一个 SwiftUI 视图,显示意图的结果:
func perform() async throws -> some IntentResult {
return .result(view: Text("Order placed!").font(.title))
}
返回带有后续操作按钮的交互式片段:
func perform() async throws -> some IntentResult {
let landmark = await findNearestLandmark()
return .result(
value: landmark,
opensIntent: OpenLandmarkIntent(landmark: landmark),
snippetIntent: LandmarkSnippetIntent(landmark: landmark)
)
}
struct LandmarkSnippetIntent: SnippetIntent {
@Parameter var landmark: LandmarkEntity
var snippet: some View {
VStack {
Text(landmark.name).font(.headline)
Text(landmark.description).font(.body)
HStack {
Button("Add to Favorites") { /* action */ }
Button("Search Tickets") { /* action */ }
}
}
.padding()
}
}
在 Swift 包和静态库中包含 App Intents:
// 在你的框架或动态库中
public struct LandmarksKitPackage: AppIntentsPackage { }
// 在你的应用目标中
struct LandmarksPackage: AppIntentsPackage {
static var includedPackages: [any AppIntentsPackage.Type] {
[LandmarksKitPackage.self]
}
}
这支持跨包边界的模块化意图定义。应用目标通过 includedPackages 聚合所有包。
快捷指令中的使用模型操作(iOS 18.1+)允许用户将 Apple Intelligence 模型纳入其自动化工作流。你的应用实体可以传递给语言模型进行过滤、转换和推理。
关键能力 在底层,该操作将你的实体的 JSON 表示传递给模型,因此你需要确保在实体定义中暴露任何你希望模型能够推理的信息。
AttributedString 类型以保留格式模型接收你的实体的 JSON 表示,包括:
1. 所有暴露的属性(转换为字符串)
struct EventEntity: AppEntity {
var id: UUID
@Property(title: "Title")
var title: String
@Property(title: "Start Date")
var startDate: Date
@Property(title: "End Date")
var endDate: Date
@Property(title: "Notes")
var notes: String?
// 所有 @Property 值都包含在模型的 JSON 中
}
2. 类型显示表示(提示实体代表什么)
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Calendar Event"
3. 显示表示(标题和副标题)
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: "\(startDate.formatted())"
)
}
{
"type": "Calendar Event",
"title": "Team Meeting",
"subtitle": "Jan 15, 2025 at 2:00 PM",
"properties": {
"Title": "Team Meeting",
"Start Date": "2025-01-15T14:00:00Z",
"End Date": "2025-01-15T15:00:00Z",
"Notes": "Discuss Q1 roadmap"
}
}
为什么重要 如果你的应用支持富文本内容,现在是时候确保你的应用意图在适当的地方对文本参数使用 attributed string 类型。
struct CreateNoteIntent: AppIntent {
@Parameter(title: "Content")
var content: String // 丢失模型的格式
}
struct CreateNoteIntent: AppIntent {
@Parameter(title: "Content")
var content: AttributedString // 保留富文本
func perform() async throws -> some IntentResult {
let note = Note(content: content) // 富文本保留
try await NoteService.shared.save(note)
return .result()
}
}
Bear 应用的创建笔记接受 AttributedString,允许来自 ChatGPT 的日记模板包含:
当使用模型输出连接到另一个操作时,运行时会自动转换类型:
// 用户的快捷指令:
// 1. 获取今天创建的笔记
// 2. 对于每个笔记:
// - 使用模型:"这篇笔记是否与为快捷指令开发功能相关?"
// - 如果 [模型输出] = 是:
// - 添加到快捷指令项目文件夹
模型不会返回冗长的文本,如 "是的,这篇笔记似乎是关于为快捷指令应用开发功能的",而是当连接到 If 操作时自动返回布尔值(true/false)。
在传递给下一个操作之前启用迭代优化:
// 用户运行快捷指令:
// 1. 从 Safari 获取食谱
// 2. 使用模型:"提取配料列表"
// - 后续:启用
// - 用户输入:"将食谱加倍"
// - 模型调整:800g 面粉而不是 400g
// 3. 添加到 Things 应用中的购物清单
IndexedEntity 通过从你的 Spotlight 集成自动生成查找操作,显著减少了样板代码。无需手动实现 EntityQuery 和 EntityPropertyQuery,采用 IndexedEntity 即可获得:
struct EventEntity: AppEntity, IndexedEntity {
var id: UUID
// 1. 带有索引键的属性
@Property(title: "Title", indexingKey: \.eventTitle)
var title: String
@Property(title: "Start Date", indexingKey: \.startDate)
var startDate: Date
@Property(title: "End Date", indexingKey: \.endDate)
var endDate: Date
// 2. 用于没有标准 Spotlight 属性的自定义键
@Property(title: "Notes", customIndexingKey: "eventNotes")
var notes: String?
// 显示表示自动映射到 Spotlight
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: "\(startDate.formatted())"
// title → kMDItemTitle
// subtitle → kMDItemDescription
// image → kMDItemContentType(如果提供)
)
}
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Event"
}
// 事件的常见 Spotlight 键
@Property(title: "Title", indexingKey: \.eventTitle)
var title: String
@Property(title: "Start Date", indexingKey: \.startDate)
var startDate: Date
@Property(title: "Location", indexingKey: \.eventLocation)
var location: String?
@Property(title: "Notes", customIndexingKey: "eventNotes")
var notes: String?
@Property(title: "Attendee Count", customIndexingKey: "attendeeCount")
var attendeeCount: Int
通过 IndexedEntity 一致性,用户自动获得此查找操作:
Find Events where:
- Title contains "Team"
- Start Date is today
- Location is "San Francisco"
EnumerableEntityQuery 协议EntityPropertyQuery 协议使用 IndexedEntity 只需添加索引键,完成!
通过实现 EntityStringQuery 启用基于字符串的搜索:
extension EventEntityQuery: EntityStringQuery {
func entities(matching string: String) async throws -> [EventEntity] {
return try await EventService.shared.search(query: string)
}
}
或者依赖 IndexedEntity + Spotlight 进行自动搜索。
对于需要自定义可搜索属性或手动索引管理的实体:
extension LandmarkEntity {
var searchableAttributes: CSSearchableItemAttributeSet {
let attributes = CSSearchableItemAttributeSet()
attributes.title = name
attributes.namedLocation = regionDescription
attributes.keywords = activities
attributes.latitude = NSNumber(value: coordinate.latitude)
attributes.longitude = NSNumber(value: coordinate.longitude)
attributes.supportsNavigation = true
return attributes
}
}
// 将实体添加到索引
func indexLandmarks() async {
let landmarks = await fetchLandmarks()
try await CSSearchableIndex.default().indexAppEntities(landmarks, priority: .normal)
}
// 删除时从索引中移除
func deleteLandmark(_ landmark: LandmarkEntity) async {
await dataStore.delete(landmark)
try await CSSearchableIndex.default().deleteAppEntities(
identifiedBy: [landmark.id],
ofType: LandmarkEntity.self
)
}
Apple 的示例代码(App Intents Travel Tracking App)演示了 IndexedEntity:
struct TripEntity: AppEntity, IndexedEntity {
var id: UUID
@Property(title: "Name", indexingKey: \.title)
var name: String
@Property(title: "Start Date", indexingKey: \.startDate)
var startDate: Date
@Property(title: "End Date", indexingKey: \.endDate)
var endDate: Date
@Property(title: "Destination", customIndexingKey: "destination")
var destination: String
// 自动生成的查找旅行操作,包含所有属性的过滤器
}
Mac 上的聚焦搜索(macOS Sequoia+)允许用户直接从系统搜索运行你的应用意图。在快捷指令中工作的意图在正确配置后会自动在聚焦搜索中工作。
关键原则 聚焦搜索的核心是快速运行操作。为此,人们需要能够直接在聚焦搜索中提供你的意图运行所需的所有信息。
参数摘要(人们将在聚焦搜索 UI 中看到的内容)必须包含所有没有默认值的必需参数。
struct CreateEventIntent: AppIntent {
static var title: LocalizedStringResource = "Create Event"
@Parameter(title: "Title")
var title: String
@Parameter(title: "Start Date")
var startDate: Date
@Parameter(title: "End Date")
var endDate: Date
@Parameter(title: "Notes") // 必需,无默认值
var notes: String
static var parameterSummary: some ParameterSummary {
Summary("Create '\(\.$title)' from \(\.$startDate) to \(\.$endDate)")
// 缺少 'notes' 参数!
}
}
@Parameter(title: "Notes")
var notes: String? // 可选 - 可以从摘要中省略
@Parameter(title: "Notes")
var notes: String = "" // 有默认值 - 可以从摘要中省略
static var parameterSummary: some ParameterSummary {
Summary("Create '\(\.$title)' from \(\.$startDate) to \(\.$endDate)") {
\.$notes // 所有必需参数都包含在内
}
}
从快捷指令隐藏的意图不会出现在聚焦搜索中:
// ❌ 从聚焦搜索隐藏
static var isDiscoverable: Bool = false
// ❌ 从聚焦搜索隐藏
static var assistantOnly: Bool = true
// ❌ 从聚焦搜索隐藏
// 没有 perform() 方法的意图(仅用于小组件配置)
通过建议使参数填充快速:
struct EventEntityQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [EventEntity] {
return try await EventService.shared.fetchEvents(ids: identifiers)
}
// 提供即将发生的事件,而不是所有过去/现在的事件
func suggestedEntities() async throws -> [EventEntity] {
return try await EventService.shared.upcomingEvents(limit: 10)
}
}
struct TimezoneQuery: EnumerableEntityQuery {
func allEntities() async throws -> [TimezoneEntity] {
// 小型列表 - 提供所有
return TimezoneEntity.allTimezones
}
}
使用建议实体当 列表很大或无限(日历事件、笔记、联系人)
使用所有实体当 列表很小且有界(时区、优先级级别、类别)
建议当前活动的内容:
// 在你的详细视图控制器中
func showEventDetail(_ event: Event) {
let activity = NSUserActivity(activityType: "com.myapp.viewEvent")
activity.persistentIdentifier = event.id.uuidString
// 聚焦搜索建议此事件用于参数
activity.appEntityIdentifier = event.id.uuidString
userActivity = activity
}
有关屏幕内容标记的更多详细信息,请参阅 "Exploring New Advances in App Intents" 会议。
基本过滤(自动):如果你提供建议,聚焦搜索会在用户输入时自动过滤它们。
深度搜索(需要实现):用于搜索超出建议的内容:
extension EventQuery: EntityStringQuery {
func entities(matching string: String) async throws -> [EventEntity] {
return try await EventService.shared.search(query: string)
}
}
struct EventEntity: AppEntity, IndexedEntity {
// 自动支持聚焦搜索
}
// 后台意图 - 无需打开应用即可运行
struct CreateEventIntent: AppIntent {
static var openAppWhenRun: Bool = false
@Parameter(title: "Title")
var title: String
@Parameter(title: "Start Date")
var startDate: Date
func perform() async throws -> some IntentResult {
let event = try await EventService.shared.createEvent(
title: title,
startDate: startDate
)
// 可选地打开应用以查看创建的事件
return .result(
value: EventEntity(from: event),
opensIntent: OpenEventIntent(event: EventEntity(from: event))
)
}
}
// 前台意图 - 打开应用到特定事件
struct OpenEventIntent: AppIntent {
static var openAppWhenRun: Bool = true
@Parameter(title: "Event")
var event: EventEntity
func perform() async throws -> some IntentResult {
await MainActor.run {
EventCoordinator.shared.showEvent(id: event.id)
}
return .result()
}
}
基于使用模式启用聚焦搜索建议:
struct OrderCoffeeIntent: AppIntent, PredictableIntent {
static var title: LocalizedStringResource = "Order Coffee"
@Parameter(title: "Coffee Type")
var coffeeType: CoffeeType
@Parameter(title: "Size")
var size: CoffeeSize
func perform() async throws -> some IntentResult {
// 订单逻辑
return .result()
}
}
聚焦搜索学习用户何时/如何运行此意图,并主动显示建议。
个人自动化 随 Mac 特定触发器一起登陆 macOS(macOS Sequoia+):
示例用例 每当新发票添加到 ~/Documents/Invoices 文件夹时,发票处理快捷指令自动运行。
只要你的意图在 macOS 上可用,它们也将可用于在快捷指令中作为 Mac 上自动化的一部分运行。这包括可在 macOS 上安装的 iOS 应用。
无需额外代码 — 你现有的意图在自动化中自动工作。
struct ProcessInvoiceIntent: AppIntent {
static var title: LocalizedStringResource = "Process Invoice"
// 在 macOS 上自动可用
// 也适用于:安装在 Mac 上的 iOS 应用(Catalyst、Mac Catalyst)
@Parameter(title: "Invoice")
var invoice: FileEntity
func perform() async throws -> some IntentResult {
// 提取数据,添加到电子表格等
return .result()
}
}
通过自动化,你的意图现在可以从以下位置访问:
Apple 为常见的应用类别提供预构建模式:
import AppIntents
import BooksIntents
struct OpenBookIntent: BooksOpenBookIntent {
@Parameter(title: "Book")
var target: BookEntity
func perform() async throws -> some IntentResult {
await MainActor.run {
BookReader.shared.open(book: target)
}
return .result()
}
}
Comprehensive guide to App Intents framework for exposing app functionality to Siri, Apple Intelligence, Shortcuts, Spotlight, and other system experiences. Replaces older SiriKit custom intents with modern Swift-first API.
Core principle App Intents make your app's actions discoverable across Apple's ecosystem. Well-designed intents feel natural in Siri conversations, Shortcuts automation, and Spotlight search.
App Intents integrate with:
Allow users to circle objects in the Visual Intelligence camera and see matching results from your app:
@UnionValue
enum VisualSearchResult {
case landmark(LandmarkEntity)
case collection(CollectionEntity)
}
struct LandmarkIntentValueQuery: IntentValueQuery {
func values(for input: SemanticContentDescriptor) async throws -> [VisualSearchResult] {
// Match visual input to app entities
}
}
// Each entity type needs an OpenIntent
struct OpenLandmarkIntent: OpenIntent { /* ... */ }
struct OpenCollectionIntent: OpenIntent { /* ... */ }
Associate app entities with visible content so users can ask Siri or ChatGPT about what's on screen:
struct LandmarkDetailView: View {
let landmark: LandmarkEntity
var body: some View {
Group { /* View content */ }
.userActivity("com.landmarks.ViewingLandmark") { activity in
activity.title = "Viewing \(landmark.name)"
activity.appEntityIdentifier = EntityIdentifier(for: landmark)
}
}
}
1. AppIntent — Executable actions with parameters
struct OrderSoupIntent: AppIntent {
static var title: LocalizedStringResource = "Order Soup"
static var description: IntentDescription = "Orders soup from the restaurant"
@Parameter(title: "Soup")
var soup: SoupEntity
@Parameter(title: "Quantity")
var quantity: Int?
func perform() async throws -> some IntentResult {
guard let quantity = quantity, quantity < 10 else {
throw $quantity.needsValue("Please specify how many soups")
}
try await OrderService.shared.order(soup: soup, quantity: quantity)
return .result()
}
}
2. AppEntity — Objects users interact with
struct SoupEntity: AppEntity {
var id: String
var name: String
var price: Decimal
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Soup"
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name)", subtitle: "$\(price)")
}
static var defaultQuery = SoupQuery()
}
3. AppEnum — Enumeration types for parameters
enum SoupSize: String, AppEnum {
case small
case medium
case large
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Size"
static var caseDisplayRepresentations: [SoupSize: DisplayRepresentation] = [
.small: "Small (8 oz)",
.medium: "Medium (12 oz)",
.large: "Large (16 oz)"
]
}
struct SendMessageIntent: AppIntent {
// REQUIRED: Short verb-noun phrase
static var title: LocalizedStringResource = "Send Message"
// REQUIRED: Purpose explanation
static var description: IntentDescription = "Sends a message to a contact"
// OPTIONAL: Discovery in Shortcuts/Spotlight
static var isDiscoverable: Bool = true
// OPTIONAL: Launch app when run
static var openAppWhenRun: Bool = false
// OPTIONAL: Authentication requirement
static var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication
}
struct BookAppointmentIntent: AppIntent {
// Required parameter (non-optional)
@Parameter(title: "Service")
var service: ServiceEntity
// Optional parameter
@Parameter(title: "Preferred Date")
var preferredDate: Date?
// Parameter with requestValueDialog for disambiguation
@Parameter(title: "Location",
requestValueDialog: "Which location would you like to visit?")
var location: LocationEntity
// Parameter with default value
@Parameter(title: "Duration")
var duration: Int = 60
}
struct OrderIntent: AppIntent {
@Parameter(title: "Item")
var item: MenuItem
@Parameter(title: "Quantity")
var quantity: Int
static var parameterSummary: some ParameterSummary {
Summary("Order \(\.$quantity) \(\.$item)") {
\.$quantity
\.$item
}
}
}
// Siri: "Order 2 lattes"
func perform() async throws -> some IntentResult {
// 1. Validate parameters
guard quantity > 0 && quantity < 100 else {
throw ValidationError.invalidQuantity
}
// 2. Execute action
let order = try await orderService.placeOrder(
item: item,
quantity: quantity
)
// 3. Donate for learning (optional)
await donation()
// 4. Return result
return .result(
value: order,
dialog: "Your order for \(quantity) \(item.name) has been placed"
)
}
enum OrderError: Error, CustomLocalizedStringResourceConvertible {
case outOfStock(itemName: String)
case paymentFailed
case networkError
var localizedStringResource: LocalizedStringResource {
switch self {
case .outOfStock(let name):
return "Sorry, \(name) is out of stock"
case .paymentFailed:
return "Payment failed. Please check your payment method"
case .networkError:
return "Network error. Please try again"
}
}
}
func perform() async throws -> some IntentResult {
if !item.isInStock {
throw OrderError.outOfStock(itemName: item.name)
}
// ...
}
struct BookEntity: AppEntity {
// REQUIRED: Unique, persistent identifier
var id: UUID
// App data properties
var title: String
var author: String
var coverImageURL: URL?
// REQUIRED: Type display name
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Book"
// REQUIRED: Instance display
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: "by \(author)",
image: coverImageURL.map { .init(url: $0) }
)
}
// REQUIRED: Query for resolution
static var defaultQuery = BookQuery()
}
struct TaskEntity: AppEntity {
var id: UUID
@Property(title: "Title")
var title: String
@Property(title: "Due Date")
var dueDate: Date?
@Property(title: "Priority")
var priority: TaskPriority
@Property(title: "Completed")
var isCompleted: Bool
// Properties exposed to system for filtering/sorting
}
Computed properties that read directly from a source of truth (no stored value):
struct SettingsEntity: UniqueAppEntity {
@ComputedProperty
var defaultPlace: PlaceDescriptor {
UserDefaults.standard.defaultPlace
}
init() { }
}
Properties that are expensive to calculate, only fetched when explicitly requested:
struct LandmarkEntity: IndexedEntity {
@DeferredProperty
var crowdStatus: Int {
get async throws {
await modelData.getCrowdStatus(self)
}
}
}
struct BookQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [BookEntity] {
// Fetch entities by IDs
return try await BookService.shared.fetchBooks(ids: identifiers)
}
func suggestedEntities() async throws -> [BookEntity] {
// Provide suggestions (recent, favorites, etc.)
return try await BookService.shared.recentBooks(limit: 10)
}
}
// Optional: Enable string-based search
extension BookQuery: EntityStringQuery {
func entities(matching string: String) async throws -> [BookEntity] {
return try await BookService.shared.searchBooks(query: string)
}
}
// DON'T make your model conform to AppEntity
struct Book: AppEntity { // Bad - couples model to intents
var id: UUID
var title: String
// ...
}
// Your core model
struct Book {
var id: UUID
var title: String
var isbn: String
var pages: Int
// ... lots of internal properties
}
// Separate entity for intents
struct BookEntity: AppEntity {
var id: UUID
var title: String
var author: String
// Convert from model
init(from book: Book) {
self.id = book.id
self.title = book.title
self.author = book.author.name
}
}
struct ViewAccountIntent: AppIntent {
// No authentication required
static var authenticationPolicy: IntentAuthenticationPolicy = .alwaysAllowed
}
struct TransferMoneyIntent: AppIntent {
// Requires user to be logged in
static var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication
}
struct UnlockVaultIntent: AppIntent {
// Requires device unlock (Face ID/Touch ID/passcode)
static var authenticationPolicy: IntentAuthenticationPolicy = .requiresLocalDeviceAuthentication
}
Use supportedModes for granular control over execution context instead of the boolean openAppWhenRun:
struct GetCrowdStatusIntent: AppIntent {
static let supportedModes: IntentModes = [.background, .foreground(.dynamic)]
func perform() async throws -> some ReturnsValue<Int> & ProvidesDialog {
guard await modelData.isOpen(landmark) else {
return .result(value: 0, dialog: "The landmark is currently closed.")
}
if systemContext.currentMode.canContinueInForeground {
do {
try await continueInForeground(alwaysConfirm: false)
await navigator.navigateToCrowdStatus(landmark)
} catch {
// Opening app was denied
}
}
let status = await modelData.getCrowdStatus(landmark)
return .result(value: status, dialog: "Current crowd level: \(status)")
}
}
| Mode | Behavior |
|---|---|
.background | Performs entirely in background |
.foreground(.immediate) | App foregrounded before perform() runs |
.foreground(.dynamic) | Can request foreground during execution |
.foreground(.deferred) | Background initially, foreground before completion |
| Combination | Use When |
|---|---|
[.background, .foreground] | Foreground default, background fallback |
[.background, .foreground(.dynamic)] | Background default, can request foreground |
[.background, .foreground(.deferred)] | Background initially, guaranteed foreground when requested |
Request foreground transition at runtime when using .foreground(.dynamic):
// Normal transition
try await continueInForeground(alwaysConfirm: false)
// Transition after an error
throw needsToContinueInForegroundError(
IntentDialog("Need to open app to complete this action"),
alwaysConfirm: true
)
struct QuickToggleIntent: AppIntent {
static var openAppWhenRun: Bool = false // Runs in background
func perform() async throws -> some IntentResult {
// Executes without opening app
await SettingsService.shared.toggle(setting: .darkMode)
return .result()
}
}
struct EditDocumentIntent: AppIntent {
@Parameter(title: "Document")
var document: DocumentEntity
func perform() async throws -> some IntentResult {
// Open app to continue in UI
return .result(opensIntent: OpenDocumentIntent(document: document))
}
}
struct OpenDocumentIntent: AppIntent {
static var openAppWhenRun: Bool = true
@Parameter(title: "Document")
var document: DocumentEntity
func perform() async throws -> some IntentResult {
// App is now foreground, safe to update UI
await MainActor.run {
DocumentCoordinator.shared.open(document: document)
}
return .result()
}
}
struct DeleteTaskIntent: AppIntent {
@Parameter(title: "Task")
var task: TaskEntity
func perform() async throws -> some IntentResult {
// Request confirmation before destructive action
try await requestConfirmation(
result: .result(dialog: "Are you sure you want to delete '\(task.title)'?"),
confirmationActionName: .init(stringLiteral: "Delete")
)
// User confirmed, proceed
try await TaskService.shared.delete(task: task)
return .result(dialog: "Task deleted")
}
}
Request user input with structured options:
let options = [
IntentChoiceOption(title: "Option 1", subtitle: "Description 1"),
IntentChoiceOption(title: "Option 2", subtitle: "Description 2"),
IntentChoiceOption.cancel(title: "Not now")
]
let choice = try await requestChoice(
between: options,
dialog: IntentDialog("Please select an option")
)
switch choice.id {
case options[0].id: // Option 1 selected
case options[1].id: // Option 2 selected
default: // Cancelled
}
Return a SwiftUI view showing the outcome of an intent:
func perform() async throws -> some IntentResult {
return .result(view: Text("Order placed!").font(.title))
}
Return interactive snippets with follow-up action buttons:
func perform() async throws -> some IntentResult {
let landmark = await findNearestLandmark()
return .result(
value: landmark,
opensIntent: OpenLandmarkIntent(landmark: landmark),
snippetIntent: LandmarkSnippetIntent(landmark: landmark)
)
}
struct LandmarkSnippetIntent: SnippetIntent {
@Parameter var landmark: LandmarkEntity
var snippet: some View {
VStack {
Text(landmark.name).font(.headline)
Text(landmark.description).font(.body)
HStack {
Button("Add to Favorites") { /* action */ }
Button("Search Tickets") { /* action */ }
}
}
.padding()
}
}
Include App Intents in Swift Packages and static libraries:
// In your framework or dynamic library
public struct LandmarksKitPackage: AppIntentsPackage { }
// In your app target
struct LandmarksPackage: AppIntentsPackage {
static var includedPackages: [any AppIntentsPackage.Type] {
[LandmarksKitPackage.self]
}
}
This enables modular intent definitions across package boundaries. The app target aggregates all packages via includedPackages.
The Use Model action in Shortcuts (iOS 18.1+) allows users to incorporate Apple Intelligence models into their automation workflows. Your app's entities can be passed to language models for filtering, transformation, and reasoning.
Key capability Under the hood, the action passes a JSON representation of your entity to the model, so you'll want to make sure to expose any information you want it to be able to reason over, in the entity definition.
AttributedString type for text parameters to preserve formattingModels receive a JSON representation of your entities including:
1. All exposed properties (converted to strings)
struct EventEntity: AppEntity {
var id: UUID
@Property(title: "Title")
var title: String
@Property(title: "Start Date")
var startDate: Date
@Property(title: "End Date")
var endDate: Date
@Property(title: "Notes")
var notes: String?
// All @Property values included in JSON for model
}
2. Type display representation (hints what entity represents)
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Calendar Event"
3. Display representation (title and subtitle)
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: "\(startDate.formatted())"
)
}
{
"type": "Calendar Event",
"title": "Team Meeting",
"subtitle": "Jan 15, 2025 at 2:00 PM",
"properties": {
"Title": "Team Meeting",
"Start Date": "2025-01-15T14:00:00Z",
"End Date": "2025-01-15T15:00:00Z",
"Notes": "Discuss Q1 roadmap"
}
}
Why it matters If your app supports Rich Text content, now is the time to make sure your app intents use the attributed string type for text parameters where appropriate.
struct CreateNoteIntent: AppIntent {
@Parameter(title: "Content")
var content: String // Loses formatting from model
}
struct CreateNoteIntent: AppIntent {
@Parameter(title: "Content")
var content: AttributedString // Preserves Rich Text
func perform() async throws -> some IntentResult {
let note = Note(content: content) // Rich Text preserved
try await NoteService.shared.save(note)
return .result()
}
}
Bear app's Create Note accepts AttributedString, allowing diary templates from ChatGPT to include:
When Use Model output connects to another action, the runtime automatically converts types:
// User's shortcut:
// 1. Get notes created today
// 2. For each note:
// - Use Model: "Is this note related to developing features for Shortcuts?"
// - If [model output] = yes:
// - Add to Shortcuts Projects folder
Instead of returning verbose text like "Yes, this note seems to be about developing features for the Shortcuts app", the model automatically returns a Boolean (true/false) when connected to an If action.
Enable iterative refinement before passing to next action:
// User runs shortcut:
// 1. Get recipe from Safari
// 2. Use Model: "Extract ingredients list"
// - Follow Up: enabled
// - User types: "Double the recipe"
// - Model adjusts: 800g flour instead of 400g
// 3. Add to Grocery List in Things app
IndexedEntity dramatically reduces boilerplate by auto-generating Find actions from your Spotlight integration. Instead of manually implementing EntityQuery and EntityPropertyQuery, adopt IndexedEntity to get:
struct EventEntity: AppEntity, IndexedEntity {
var id: UUID
// 1. Properties with indexing keys
@Property(title: "Title", indexingKey: \.eventTitle)
var title: String
@Property(title: "Start Date", indexingKey: \.startDate)
var startDate: Date
@Property(title: "End Date", indexingKey: \.endDate)
var endDate: Date
// 2. Custom key for properties without standard Spotlight attribute
@Property(title: "Notes", customIndexingKey: "eventNotes")
var notes: String?
// Display representation automatically maps to Spotlight
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: "\(startDate.formatted())"
// title → kMDItemTitle
// subtitle → kMDItemDescription
// image → kMDItemContentType (if provided)
)
}
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Event"
}
// Common Spotlight keys for events
@Property(title: "Title", indexingKey: \.eventTitle)
var title: String
@Property(title: "Start Date", indexingKey: \.startDate)
var startDate: Date
@Property(title: "Location", indexingKey: \.eventLocation)
var location: String?
@Property(title: "Notes", customIndexingKey: "eventNotes")
var notes: String?
@Property(title: "Attendee Count", customIndexingKey: "attendeeCount")
var attendeeCount: Int
With IndexedEntity conformance, users get this Find action automatically:
Find Events where:
- Title contains "Team"
- Start Date is today
- Location is "San Francisco"
EnumerableEntityQuery protocolEntityPropertyQuery protocolWith IndexedEntity Just add indexing keys, done!
Enable string-based search by implementing EntityStringQuery:
extension EventEntityQuery: EntityStringQuery {
func entities(matching string: String) async throws -> [EventEntity] {
return try await EventService.shared.search(query: string)
}
}
Or rely on IndexedEntity + Spotlight for automatic search.
For entities that need custom searchable attributes or manual index management:
extension LandmarkEntity {
var searchableAttributes: CSSearchableItemAttributeSet {
let attributes = CSSearchableItemAttributeSet()
attributes.title = name
attributes.namedLocation = regionDescription
attributes.keywords = activities
attributes.latitude = NSNumber(value: coordinate.latitude)
attributes.longitude = NSNumber(value: coordinate.longitude)
attributes.supportsNavigation = true
return attributes
}
}
// Add entities to index
func indexLandmarks() async {
let landmarks = await fetchLandmarks()
try await CSSearchableIndex.default().indexAppEntities(landmarks, priority: .normal)
}
// Remove from index when deleted
func deleteLandmark(_ landmark: LandmarkEntity) async {
await dataStore.delete(landmark)
try await CSSearchableIndex.default().deleteAppEntities(
identifiedBy: [landmark.id],
ofType: LandmarkEntity.self
)
}
Apple's sample code (App Intents Travel Tracking App) demonstrates IndexedEntity:
struct TripEntity: AppEntity, IndexedEntity {
var id: UUID
@Property(title: "Name", indexingKey: \.title)
var name: String
@Property(title: "Start Date", indexingKey: \.startDate)
var startDate: Date
@Property(title: "End Date", indexingKey: \.endDate)
var endDate: Date
@Property(title: "Destination", customIndexingKey: "destination")
var destination: String
// Auto-generated Find Trips action with filters for all properties
}
Spotlight on Mac (macOS Sequoia+) allows users to run your app's intents directly from system search. Intents that work in Shortcuts automatically work in Spotlight with proper configuration.
Key principle Spotlight is all about running things quickly. To do that, people need to be able to provide all the information your intent needs to run directly in Spotlight.
The parameter summary, which is what people will see in Spotlight UI, must contain all required parameters that don't have a default value.
struct CreateEventIntent: AppIntent {
static var title: LocalizedStringResource = "Create Event"
@Parameter(title: "Title")
var title: String
@Parameter(title: "Start Date")
var startDate: Date
@Parameter(title: "End Date")
var endDate: Date
@Parameter(title: "Notes") // Required, no default
var notes: String
static var parameterSummary: some ParameterSummary {
Summary("Create '\(\.$title)' from \(\.$startDate) to \(\.$endDate)")
// Missing 'notes' parameter!
}
}
@Parameter(title: "Notes")
var notes: String? // Optional - can omit from summary
@Parameter(title: "Notes")
var notes: String = "" // Has default - can omit from summary
static var parameterSummary: some ParameterSummary {
Summary("Create '\(\.$title)' from \(\.$startDate) to \(\.$endDate)") {
\.$notes // All required params included
}
}
Intents hidden from Shortcuts won't appear in Spotlight:
// ❌ Hidden from Spotlight
static var isDiscoverable: Bool = false
// ❌ Hidden from Spotlight
static var assistantOnly: Bool = true
// ❌ Hidden from Spotlight
// Intent with no perform() method (widget configuration only)
Make parameter filling quick with suggestions:
struct EventEntityQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [EventEntity] {
return try await EventService.shared.fetchEvents(ids: identifiers)
}
// Provide upcoming events, not all past/present events
func suggestedEntities() async throws -> [EventEntity] {
return try await EventService.shared.upcomingEvents(limit: 10)
}
}
struct TimezoneQuery: EnumerableEntityQuery {
func allEntities() async throws -> [TimezoneEntity] {
// Small list - provide all
return TimezoneEntity.allTimezones
}
}
Use suggested entities when List is large or unbounded (calendar events, notes, contacts) Use all entities when List is small and bounded (timezones, priority levels, categories)
Suggest currently active content:
// In your detail view controller
func showEventDetail(_ event: Event) {
let activity = NSUserActivity(activityType: "com.myapp.viewEvent")
activity.persistentIdentifier = event.id.uuidString
// Spotlight suggests this event for parameters
activity.appEntityIdentifier = event.id.uuidString
userActivity = activity
}
For more details on on-screen content tagging, see the "Exploring New Advances in App Intents" session.
Basic filtering (automatic): If you provide suggestions, Spotlight automatically filters them as user types.
Deep search (requires implementation): For searching beyond suggestions:
extension EventQuery: EntityStringQuery {
func entities(matching string: String) async throws -> [EventEntity] {
return try await EventService.shared.search(query: string)
}
}
struct EventEntity: AppEntity, IndexedEntity {
// Spotlight search automatically supported
}
// Background intent - runs without opening app
struct CreateEventIntent: AppIntent {
static var openAppWhenRun: Bool = false
@Parameter(title: "Title")
var title: String
@Parameter(title: "Start Date")
var startDate: Date
func perform() async throws -> some IntentResult {
let event = try await EventService.shared.createEvent(
title: title,
startDate: startDate
)
// Optionally open app to view created event
return .result(
value: EventEntity(from: event),
opensIntent: OpenEventIntent(event: EventEntity(from: event))
)
}
}
// Foreground intent - opens app to specific event
struct OpenEventIntent: AppIntent {
static var openAppWhenRun: Bool = true
@Parameter(title: "Event")
var event: EventEntity
func perform() async throws -> some IntentResult {
await MainActor.run {
EventCoordinator.shared.showEvent(id: event.id)
}
return .result()
}
}
Enable Spotlight suggestions based on usage patterns:
struct OrderCoffeeIntent: AppIntent, PredictableIntent {
static var title: LocalizedStringResource = "Order Coffee"
@Parameter(title: "Coffee Type")
var coffeeType: CoffeeType
@Parameter(title: "Size")
var size: CoffeeSize
func perform() async throws -> some IntentResult {
// Order logic
return .result()
}
}
Spotlight learns when/how user runs this intent and surfaces suggestions proactively.
Personal Automations arrive on macOS (macOS Sequoia+) with Mac-specific triggers:
Example use case Invoice processing shortcut runs automatically every time a new invoice is added to ~/Documents/Invoices folder.
As long as your intent is available on macOS, they will also be available to use in Shortcuts to run as a part of Automations on Mac. This includes iOS apps that are installable on macOS.
No additional code required — your existing intents work in automations automatically.
struct ProcessInvoiceIntent: AppIntent {
static var title: LocalizedStringResource = "Process Invoice"
// Available on macOS automatically
// Also works: iOS apps installed on Mac (Catalyst, Mac Catalyst)
@Parameter(title: "Invoice")
var invoice: FileEntity
func perform() async throws -> some IntentResult {
// Extract data, add to spreadsheet, etc.
return .result()
}
}
With automations, your intents are now accessible from:
Apple provides pre-built schemas for common app categories:
import AppIntents
import BooksIntents
struct OpenBookIntent: BooksOpenBookIntent {
@Parameter(title: "Book")
var target: BookEntity
func perform() async throws -> some IntentResult {
await MainActor.run {
BookReader.shared.open(book: target)
}
return .result()
}
}
Add intent to Shortcuts :
Test parameter resolution :
Test with Siri :
// In your app target, not tests
#if DEBUG
extension OrderSoupIntent {
static func testIntent() async throws {
let intent = OrderSoupIntent()
intent.soup = SoupEntity(id: "1", name: "Tomato", price: 8.99)
intent.quantity = 2
let result = try await intent.perform()
print("Result: \(result)")
}
}
#endif
// ❌ Problem: isDiscoverable = false or missing
struct MyIntent: AppIntent {
// Missing isDiscoverable
}
// ✅ Solution: Make discoverable
struct MyIntent: AppIntent {
static var isDiscoverable: Bool = true
}
// ❌ Problem: Missing defaultQuery
struct ProductEntity: AppEntity {
var id: String
// Missing defaultQuery
}
// ✅ Solution: Add query
struct ProductEntity: AppEntity {
var id: String
static var defaultQuery = ProductQuery()
}
// ❌ Problem: Accessing MainActor from background
func perform() async throws -> some IntentResult {
UIApplication.shared.open(url) // Crash! MainActor only
return .result()
}
// ✅ Solution: Use MainActor or openAppWhenRun
func perform() async throws -> some IntentResult {
await MainActor.run {
UIApplication.shared.open(url)
}
return .result()
}
// ❌ Problem: entities(for:) not implemented
struct BookQuery: EntityQuery {
// Missing entities(for:) implementation
}
// ✅ Solution: Implement required methods
struct BookQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [BookEntity] {
return try await BookService.shared.fetchBooks(ids: identifiers)
}
func suggestedEntities() async throws -> [BookEntity] {
return try await BookService.shared.recentBooks(limit: 10)
}
}
static var title: LocalizedStringResource = "Do Thing"
static var title: LocalizedStringResource = "Process"
static var title: LocalizedStringResource = "Send Message"
static var title: LocalizedStringResource = "Book Appointment"
static var title: LocalizedStringResource = "Start Workout"
static var parameterSummary: some ParameterSummary {
Summary("Execute \(\.$action) with \(\.$target)")
}
static var parameterSummary: some ParameterSummary {
Summary("Send \(\.$message) to \(\.$contact)")
}
// Siri: "Send 'Hello' to John"
throw MyError.validationFailed("Invalid parameter state")
throw MyError.outOfStock("Sorry, this item is currently unavailable")
func suggestedEntities() async throws -> [TaskEntity] {
return try await TaskService.shared.allTasks() // Could be thousands!
}
func suggestedEntities() async throws -> [TaskEntity] {
return try await TaskService.shared.recentTasks(limit: 10)
}
func perform() async throws -> some IntentResult {
let data = URLSession.shared.synchronousDataTask(url) // Blocks!
return .result()
}
func perform() async throws -> some IntentResult {
let data = try await URLSession.shared.data(from: url)
return .result()
}
struct StartWorkoutIntent: AppIntent {
static var title: LocalizedStringResource = "Start Workout"
static var description: IntentDescription = "Starts a new workout session"
static var openAppWhenRun: Bool = true
@Parameter(title: "Workout Type")
var workoutType: WorkoutType
@Parameter(title: "Duration (minutes)")
var duration: Int?
static var parameterSummary: some ParameterSummary {
Summary("Start \(\.$workoutType)") {
\.$duration
}
}
func perform() async throws -> some IntentResult {
let workout = Workout(
type: workoutType,
duration: duration.map { TimeInterval($0 * 60) }
)
await MainActor.run {
WorkoutCoordinator.shared.start(workout)
}
return .result(
dialog: "Starting \(workoutType.displayName) workout"
)
}
}
enum WorkoutType: String, AppEnum {
case running
case cycling
case swimming
case yoga
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Workout Type"
static var caseDisplayRepresentations: [WorkoutType: DisplayRepresentation] = [
.running: "Running",
.cycling: "Cycling",
.swimming: "Swimming",
.yoga: "Yoga"
]
var displayName: String {
switch self {
case .running: return "running"
case .cycling: return "cycling"
case .swimming: return "swimming"
case .yoga: return "yoga"
}
}
}
struct AddTaskIntent: AppIntent {
static var title: LocalizedStringResource = "Add Task"
static var description: IntentDescription = "Creates a new task"
static var isDiscoverable: Bool = true
@Parameter(title: "Title")
var title: String
@Parameter(title: "List")
var list: TaskListEntity?
@Parameter(title: "Due Date")
var dueDate: Date?
static var parameterSummary: some ParameterSummary {
Summary("Add '\(\.$title)'") {
\.$list
\.$dueDate
}
}
func perform() async throws -> some IntentResult {
let task = try await TaskService.shared.createTask(
title: title,
list: list?.id,
dueDate: dueDate
)
return .result(
value: TaskEntity(from: task),
dialog: "Task '\(title)' added"
)
}
}
struct TaskListEntity: AppEntity {
var id: UUID
var name: String
var color: String
static var typeDisplayRepresentation: TypeDisplayRepresentation = "List"
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(name)",
image: .init(systemName: "list.bullet")
)
}
static var defaultQuery = TaskListQuery()
}
struct TaskListQuery: EntityQuery, EntityStringQuery {
func entities(for identifiers: [UUID]) async throws -> [TaskListEntity] {
return try await TaskService.shared.fetchLists(ids: identifiers)
}
func suggestedEntities() async throws -> [TaskListEntity] {
// Provide user's favorite lists
return try await TaskService.shared.favoriteLists(limit: 5)
}
func entities(matching string: String) async throws -> [TaskListEntity] {
return try await TaskService.shared.searchLists(query: string)
}
}
isDiscoverable appear in ShortcutsopenAppWhenRun = truedisplayRepresentation shows meaningful infoWWDC : 2025-244, 2025-275, 2025-260
Docs : /appintents, /appintents/appintent, /appintents/appentity, /Updates/AppIntents
Skills : axiom-app-shortcuts-ref, axiom-core-spotlight-ref, axiom-app-discoverability
Remember App Intents are how users interact with your app through Siri, Shortcuts, and system features. Well-designed intents feel like a natural extension of your app's functionality and provide value across Apple's ecosystem.
Weekly Installs
110
Repository
GitHub Stars
617
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
opencode94
codex89
claude-code86
gemini-cli85
cursor80
github-copilot78