mcpField
A
axiom-app-intents-ref

axiom-app-intents-ref

github.com/charleswiltgen开发1.2k184 次安装今天更新
代码仓库

关于 axiom-app-intents-ref

这是一份面向 Swift 开发者的 Apple App Intents 框架指南,用于将应用操作和内容接入各类系统体验。内容涵盖意图、实体、执行方式,以及 Siri、快捷指令、小组件和 Visual Intelligence 等集成场景。

主要功能

  1. 01定义操作、实体和枚举
  2. 02配置参数与实体查询
  3. 03控制执行方式和身份验证
  4. 04添加确认与选项流程
  5. 05返回交互式片段
  6. 06接入 Apple 系统体验

适用场景

  1. 01将应用操作接入 Siri 和快捷指令
  2. 02让应用内容可通过 Spotlight 搜索
  3. 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()
    }
}
用户体验
  1. 用户在 Spotlight 中运行“创建事件”(后台)
  2. 创建事件,不打开应用
  3. Spotlight 显示“在应用中打开”按钮(opensIntent)
  4. 用户轻点按钮 → 应用打开并显示事件详情
可预测意图协议

根据使用模式启用 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 — 文件管理、页面操作、搜索

测试与调试

使用“快捷指令”应用进行测试
  1. 将意图添加到“快捷指令”:

    • 打开“快捷指令”应用
    • 轻点“+”创建新快捷指令
    • 搜索你的应用名称
    • 选择你的意图
  2. 测试参数解析:

    • 填写参数
    • 运行快捷指令
    • 检查 Xcode 控制台中的日志
  3. 使用 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 生态系统中发挥价值。

评论

支持 Markdown