关于 axiom-app-intents-ref
这是一份面向 Swift 开发者的 Apple App Intents 框架指南,用于将应用操作和内容接入各类系统体验。内容涵盖意图、实体、执行方式,以及 Siri、快捷指令、小组件和 Visual Intelligence 等集成场景。
主要功能
- 01定义操作、实体和枚举
- 02配置参数与实体查询
- 03控制执行方式和身份验证
- 04添加确认与选项流程
- 05返回交互式片段
- 06接入 Apple 系统体验
适用场景
- 01将应用操作接入 Siri 和快捷指令
- 02让应用内容可通过 Spotlight 搜索
- 03实现 Visual Intelligence 结果查询
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-app-intents-ref安装后在 Claude Code 中输入/axiom-app-intents-ref确认已加载。
详细介绍
App Intents 集成
概述
App Intents 框架综合指南,介绍如何向 Siri、Apple Intelligence、Shortcuts、Spotlight 及其他系统体验公开应用功能。它以现代 Swift 优先的 API 取代了较早的 SiriKit 自定义意图。
核心原则 App Intents 让应用操作能够在 Apple 生态系统中被发现。设计良好的意图在 Siri 对话、Shortcuts 自动化和 Spotlight 搜索中都能带来自然的体验。
何时使用此技能
- 向 Siri 和 Apple Intelligence 公开应用功能
- 在 Shortcuts 应用中提供应用操作
- 为应用内容启用 Spotlight 搜索
- 与专注模式过滤器、小组件、实时活动集成
- 添加操作按钮支持(Apple Watch Ultra)
- 调试意图解析或参数验证失败
- 使用 Shortcuts 应用测试意图
- 为应用内容实现实体查询
相关技能
- app-shortcuts-ref — App Shortcuts 可立即用于 Siri/Spotlight,无需用户设置
- core-spotlight-ref — Core Spotlight 和 NSUserActivity 集成,用于内容索引
- app-discoverability — 通过所有 API 让应用在全系统中呈现的策略指南
支持的系统体验
App Intents 可集成到:
- Siri — 语音命令和 Apple Intelligence
- Shortcuts — 自动化工作流
- App Shortcuts — 可立即使用的预配置操作(参见 app-shortcuts-ref)
- Spotlight — 搜索发现
- 专注模式过滤器 — 情境化过滤
- 操作按钮 — 快速操作(Apple Watch Ultra)
- 控制中心 — 自定义控制项
- WidgetKit — 交互式小组件
- 实时活动 — Dynamic Island 更新
- 视觉智能 — 基于图像的交互
视觉智能集成
IntentValueQuery
允许用户在视觉智能相机中圈选对象,并查看应用中的匹配结果:
@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 { /* ... */ }
屏幕实体
将应用实体与可见内容相关联,让用户可以通过 Siri 或 ChatGPT 询问屏幕上的内容:
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 — 带参数的可执行操作
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)"
]
}
AppIntent:定义操作
必需属性
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
}
参数摘要(Siri 短语)
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"
perform() 方法
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)
}
// ...
}
AppEntity:表示应用内容
实体定义
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
}
计算属性和延迟属性
@ComputedProperty
直接从事实来源读取的计算属性(不存储值):
struct SettingsEntity: UniqueAppEntity {
@ComputedProperty
var defaultPlace: PlaceDescriptor {
UserDefaults.standard.defaultPlace
}
init() { }
}
@DeferredProperty
计算开销较大的属性,仅在明确请求时获取:
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
}
后台与前台执行
意图模式
使用 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 {
// Opening app was denied
}
}
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) 时,在运行时请求切换到前台:
// 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")
}
}
多选 API
通过结构化选项请求用户输入:
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
}
交互式片段
静态片段
返回一个显示意图执行结果的 SwiftUI 视图:
func perform() async throws -> some IntentResult {
return .result(view: Text("Order placed!").font(.title))
}
SnippetIntent
返回带后续操作按钮的交互式片段:
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 Package 支持
AppIntentsPackage
在 Swift Packages 和静态库中包含 App Intents:
// 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]
}
}
这可以在不同 Package 边界间实现模块化意图定义。应用目标通过 includedPackages 汇总所有 Package。
Apple Intelligence:使用模型操作
概述
快捷指令中的使用模型操作(iOS 18.1+)允许用户将 Apple Intelligence 模型融入自动化工作流。应用的实体可以传递给语言模型,用于筛选、转换和推理。
主要功能 在底层,该操作会将实体的 JSON 表示传递给模型,因此应确保在实体定义中公开所有希望模型据以推理的信息。
三种输出类型
1. 文本(AttributedString)
- 模型通常会返回富文本(粗体、斜体、列表、表格)
- 文本参数使用
AttributedString类型,以保留格式 - 支持从模型到应用的无损传递
2. 字典
- 从非结构化输入中提取结构化数据
- 适用于解析 PDF、电子邮件和文档
- 示例:从发票中提取供应商、金额和日期
3. 应用实体(你的类型)
- 将实体列表传递给模型以进行筛选和推理
- 模型会接收实体的 JSON 表示
- 示例:“筛选与我的旅行相关的日历事件”
向模型公开实体
模型会接收实体的 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?
// All @Property values included in JSON for model
}
2. 类型显示表示(提示实体代表什么)
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Calendar Event"
3. 显示表示(标题和副标题)
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: "\(startDate.formatted())"
)
}
发送给模型的 JSON 示例
{
"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"
}
}
使用 AttributedString 支持富文本
重要性 如果应用支持富文本内容,现在就应确保应用意图在适当情况下对文本参数使用 attributed string 类型。
❌ 不要:使用普通 String
struct CreateNoteIntent: AppIntent {
@Parameter(title: "Content")
var content: String // Loses formatting from model
}
✅ 应该:使用 AttributedString
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()
}
}
WWDC 中的实际示例
Bear 应用的“创建笔记”接受 AttributedString,因此 ChatGPT 生成的日记模板可以包含:
- 粗体标题
- 心情记录表格
- 格式化列表
- 所有内容均可无损保留
自动类型转换
当“使用模型”的输出连接到另一个操作时,运行时会自动转换类型:
示例:为 If 操作提供布尔值
// 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
连接到 If 操作时,模型会自动返回布尔值(true/false),而不是返回“是的,这条笔记似乎是在讨论为快捷指令应用开发功能”这类冗长文本。
可用的显式输出类型
- 文本(AttributedString)
- 数字
- 布尔值
- 字典
- 日期
- 应用实体
后续跟进功能
在传递给下一个操作前启用迭代优化:
// 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:自动查找操作
概述
IndexedEntity 可通过根据 Spotlight 集成自动生成查找操作,大幅减少样板代码。无需手动实现 EntityQuery 和 EntityPropertyQuery,采用 IndexedEntity 即可获得:
- 在 Shortcuts 中自动提供查找操作
- 基于属性进行筛选
- 搜索支持
- 所需代码极少
基本实现
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"
}
索引键映射
标准 Spotlight 属性键
// 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
自动生成的查找操作
符合 IndexedEntity 协议后,用户将自动获得此查找操作:
在 Shortcuts 应用中
Find Events where:
- Title contains "Team"
- Start Date is today
- Location is "San Francisco"
如果不使用 IndexedEntity,则需要手动实现
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 自动实现搜索。
显式 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
}
}
// 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 的示例代码(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
// Auto-generated Find Trips action with filters for all properties
}
Mac 上的 Spotlight
概述
Mac 上的 Spotlight(macOS Sequoia+)允许用户直接从系统搜索中运行应用的意图。只要配置正确,在 Shortcuts 中可用的意图也能在 Spotlight 中使用。
关键原则 Spotlight 的核心是快速运行操作。为此,用户需要能够直接在 Spotlight 中提供意图运行所需的所有信息。
在 Spotlight 中显示的要求
1. 参数摘要必须包含所有必填参数
参数摘要是用户在 Spotlight 界面中看到的内容,其中必须包含所有没有默认值的必填参数。
❌ 不会在 Spotlight 中显示
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!
}
}
✅ 会在 Spotlight 中显示(选项 1:设为可选)
@Parameter(title: "Notes")
var notes: String? // Optional - can omit from summary
✅ 会在 Spotlight 中显示(选项 2:提供默认值)
@Parameter(title: "Notes")
var notes: String = "" // Has default - can omit from summary
✅ 会在 Spotlight 中显示(选项 3:纳入摘要)
static var parameterSummary: some ParameterSummary {
Summary("Create '\(\.$title)' from \(\.$startDate) to \(\.$endDate)") {
\.$notes // All required params included
}
}
2. 意图不能设为隐藏
在 Shortcuts 中隐藏的意图不会出现在 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)
提供建议
通过建议加快参数填写:
选项 1:建议的实体(大型列表的子集)
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)
}
}
选项 2:所有实体(规模小且有界的列表)
struct TimezoneQuery: EnumerableEntityQuery {
func allEntities() async throws -> [TimezoneEntity] {
// Small list - provide all
return TimezoneEntity.allTimezones
}
}
在以下情况下使用建议的实体: 列表规模大或无界(日历事件、笔记、联系人) 在以下情况下使用所有实体: 列表规模小且有界(时区、优先级、类别)
屏幕内容标记
建议当前活跃的内容:
// 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
}
有关屏幕内容标记的更多详情,请参阅“探索 App Intents 的新进展”专题。
搜索建议以外的内容
基本筛选(自动): 如果提供建议,Spotlight 会在用户输入时自动筛选这些建议。
深度搜索(需要实现): 若要搜索建议以外的内容:
选项 1:EntityStringQuery
extension EventQuery: EntityStringQuery {
func entities(matching string: String) async throws -> [EventEntity] {
return try await EventService.shared.search(query: string)
}
}
选项 2:IndexedEntity
struct EventEntity: AppEntity, IndexedEntity {
// Spotlight search automatically supported
}
后台与前台意图
模式:使用 opensIntent 配对意图
// 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()
}
}
用户体验
- 用户在 Spotlight 中运行“创建事件”(后台)
- 创建事件,不打开应用
- Spotlight 显示“在应用中打开”按钮(opensIntent)
- 用户轻点按钮 → 应用打开并显示事件详情
可预测意图协议
根据使用模式启用 Spotlight 建议:
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 会学习用户运行此意图的时间和方式,并主动显示建议。
Mac 上的自动化
概述
个人自动化现已登陆 macOS(macOS Sequoia+),并提供 Mac 专属触发条件:
新增的 Mac 自动化类型
- 文件夹自动化 — 在文件夹中添加或移除文件时触发
- 外部驱动器自动化 — 连接或断开驱动器时触发
- 一天中的时间(来自 iOS)
- 蓝牙(来自 iOS)
- 以及更多……
示例用例:每当有新发票添加到 ~/Documents/Invoices 文件夹时,发票处理快捷指令就会自动运行。
自动可用性
只要你的意图在 macOS 上可用,就可以在“快捷指令”中使用它们,作为 Mac 自动化的一部分运行。这包括可安装在 macOS 上的 iOS 应用。
无需额外代码 — 你现有的意图会自动用于自动化。
平台支持
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()
}
}
其他系统集成点
借助自动化,你的意图现在可以通过以下方式访问:
- Siri — 语音命令
- “快捷指令”应用 — 手动工作流
- Spotlight — 快速操作
- 自动化 — 触发式工作流
- 操作按钮 — 硬件触发器(Apple Watch Ultra)
- 控制中心 — 快速控制
- 小组件 — 交互元素
- 实时活动 — 灵动岛
助手架构(预构建意图)
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()
}
}
可用的助手架构
- BooksIntents — 浏览页面、打开图书、播放有声书、搜索
- BrowserIntents — 为标签页添加书签、清除历史记录、管理窗口
- CameraIntents — 拍摄模式、切换设备、开始/停止
- EmailIntents — 草稿管理、回复、转发、归档
- PhotosIntents — 相册/资源管理、编辑、筛选
- PresentationsIntents — 创建幻灯片、插入媒体、播放
- SpreadsheetsIntents — 工作表管理、添加内容
- DocumentsIntents — 文件管理、页面操作、搜索
测试与调试
使用“快捷指令”应用进行测试
-
将意图添加到“快捷指令”:
- 打开“快捷指令”应用
- 轻点“+”创建新快捷指令
- 搜索你的应用名称
- 选择你的意图
-
测试参数解析:
- 填写参数
- 运行快捷指令
- 检查 Xcode 控制台中的日志
-
使用 Siri 测试:
- “嘿 Siri,[你的意图名称]”
- Siri 应提示你提供参数
- 验证对话框文本和结果
Xcode 意图测试
// 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
常见调试问题
问题 1:意图未显示在“快捷指令”中
// ❌ Problem: isDiscoverable = false or missing
struct MyIntent: AppIntent {
// Missing isDiscoverable
}
// ✅ Solution: Make discoverable
struct MyIntent: AppIntent {
static var isDiscoverable: Bool = true
}
问题 2:参数无法解析
// ❌ Problem: Missing defaultQuery
struct ProductEntity: AppEntity {
var id: String
// Missing defaultQuery
}
// ✅ Solution: Add query
struct ProductEntity: AppEntity {
var id: String
static var defaultQuery = ProductQuery()
}
问题 3:意图在后台崩溃
// ❌ 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()
}
问题 4:实体查询返回空结果
// ❌ 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)
}
}
最佳实践
1. 意图命名
❌ 不要:使用通用或含糊的名称
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"
2. 参数摘要
❌ 不要:使用技术术语或令人困惑的表述
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"
3. 错误消息
❌ 不要:使用技术术语
throw MyError.validationFailed("Invalid parameter state")
✅ 应该:使用对用户友好的表述
throw MyError.outOfStock("Sorry, this item is currently unavailable")
4. 实体建议
❌ 不要:返回所有实体
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)
}
5. 异步操作
❌ 不要:阻塞主线程
func perform() async throws -> some IntentResult {
let data = URLSession.shared.synchronousDataTask(url) // Blocks!
return .result()
}
✅ 应该:使用 async/await
func perform() async throws -> some IntentResult {
let data = try await URLSession.shared.data(from: url)
return .result()
}
实际示例
示例 1:开始锻炼意图
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"
}
}
}
示例 2:使用实体查询添加任务
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)
}
}
App Intents 检查清单
提交到 App Store 之前
- ☐ 所有意图都有清晰、本地化的标题和描述
- ☐ 参数摘要使用自然语言表述
- ☐ 错误消息对用户友好,而非技术性表述
- ☐ 身份验证策略与数据敏感度相匹配
- ☐ 实体查询返回合理数量的建议(< 20)
- ☐ 标记为
isDiscoverable的意图会显示在快捷指令中 - ☐ 执行破坏性操作前会请求确认
- ☐ 后台意图不会访问 MainActor
- ☐ 前台意图设置
openAppWhenRun = true - ☐ 实体的
displayRepresentation会显示有意义的信息 - ☐ 已使用 Siri 语音指令进行测试
- ☐ 已在快捷指令 App 中进行测试
- ☐ 已使用不同的参数组合进行测试
- ☐ 已验证所有支持语言的本地化
资源
WWDC:2025-244、2025-275、2025-260
文档:/appintents、/appintents/appintent、/appintents/appentity、/Updates/AppIntents
技能:axiom-app-shortcuts-ref、axiom-core-spotlight-ref、axiom-app-discoverability
请记住,App Intents 是用户通过 Siri、快捷指令和系统功能与你的 App 交互的方式。设计良好的意图就像 App 功能的自然延伸,并能在 Apple 生态系统中发挥价值。