mcpField
A
app-intents

app-intents

github.com/dpearson2699開発1.2k4.0k 回インストール13 週間前に更新
リポジトリ

app-intents について

iOS App Intents の実装とレビューを支援する開発スキルです。アプリの操作やエンティティを Siri、Shortcuts、Spotlight、ウィジェット、コントロールセンター、Apple Intelligence に公開する方法を扱い、iOS 26 の API も含みます。

主な機能

  1. 01AppIntent とパラメーター設計
  2. 02AppEntity とエンティティクエリ
  3. 03AppEnum とショートカットフレーズ
  4. 04システム画面との連携指針
  5. 05iOS 26 のスニペットとビジュアルクエリ
  6. 06実装レビューのチェックリスト

活用シーン

  1. 01Siri や Shortcuts からアプリ操作を実行
  2. 02Spotlight 検索向けにアプリのエンティティを提供
  3. 03ウィジェットやコントロールセンターの操作を設定

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill app-intents

インストール後、Claude Code で/app-intentsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

evals/
evals.json
references/
appintents-advanced.md
system-surfaces.md

詳細

App Intents(iOS 26 以降)

App Intents を実装、レビュー、拡張して、Siri、 ショートカット、Spotlight、ウィジェット、コントロールセンター、Apple Intelligence にアプリの機能を公開します。

目次

トリアージワークフロー

ステップ 1:アクション、境界、サーフェスを選ぶ

画面階層ではなく、ユーザーがアプリの外で実行したい価値のあるアクションを 1~3 個選ぶことから始めます。アクションごとに設計項目を 1 行にまとめます。ユーザーの目標、インラインでの結果表示かアプリへの遷移か、共有するドメイン操作、パラメーターとエンティティクエリ、確認/認証、対象サーフェスとプロトコルを記録してください。インテント内にナビゲーションの副作用を分散させるのではなく、アプリへのハンドオフには明示的なランタイムルートを 1 つ使用します。

次に、そのアクションに適したシステム機能とプロトコルを選びます。

サーフェスプロトコル対応開始
Siri/ショートカットAppIntentiOS 16
構成可能なウィジェットWidgetConfigurationIntentiOS 17
コントロールセンターControlConfigurationIntentiOS 18
Spotlight 検索IndexedEntityiOS 18
Apple Intelligence@AppIntent(schema:)iOS 18
インタラクティブスニペットSnippetIntentiOS 26
Visual IntelligenceIntentValueQueryiOS 26
ステップ 2:データモデルを定義する
  • システムに公開するアプリデータには、AppEntity のシャドウモデルを優先します。
  • 固定のパラメーター選択肢には AppEnum 型を作成します。
  • 解決に適した EntityQuery のバリエーションを選びます。
  • 検索可能なエンティティには IndexedEntity と indexingKey メタデータを設定します。
ステップ 3:インテントを実装する
  • AppIntent(または特化したサブプロトコル)に準拠させます。
  • ユーザー向けの入力すべてに @Parameter プロパティを宣言します。
  • perform() async throws -> some IntentResult を実装します。
  • ショートカットの UI 向けに parameterSummary を追加します。
  • AppShortcutsProvider を使ってフレーズを登録します。
ステップ 4:検証する
  • 対象のシステムサーフェスでビルドして実行し、検出、パラメーター解決、キャンセル、確認、認証、結果の表示、アプリへのハンドオフを確認します。
  • 手順に失敗した場合は、テスト用データをリセットし、インテント/エンティティ/クエリの境界で最小限の修正を加え、別のアクションを追加する前に、同じサーフェスから同じアクションを再実行します。
  • Xcode のインテントプレビューで Siri のフレーズをテストします。
  • IndexedEntity インスタンスが名前付き Spotlight インデックスに登録されていることを確認します。
  • WidgetConfigurationIntent インテントのウィジェット構成を確認します。

AppIntent プロトコル

システムは init() を介して構造体をインスタンス化し、パラメーターを設定してから perform() を呼び出します。ショートカットの UI 向けに title と parameterSummary を宣言します。

struct OrderSoupIntent: AppIntent {
    static var title: LocalizedStringResource = "Order Soup"
    static var description = IntentDescription("Place a soup order.")

    @Parameter(title: "Soup") var soup: SoupEntity
    @Parameter(title: "Quantity", default: 1) var quantity: Int

    static var parameterSummary: some ParameterSummary {
        Summary("Order \(\.$soup)") { \.$quantity }
    }

    func perform() async throws -> some IntentResult {
        try await OrderService.shared.place(soup: soup.id, quantity: quantity)
        return .result(dialog: "Ordered \(quantity) \(soup.name).")
    }
}

省略可能なメンバー:description(IntentDescription)、openAppWhenRun(Bool)、 isDiscoverable(Bool)、authenticationPolicy(IntentAuthenticationPolicy)。

@Parameter

ユーザー向けの入力はそれぞれ @Parameter で宣言します。省略不可のパラメーターは必須であり、必要に応じてシステムが値を要求します。デフォルト値を設定すると、役立つ値があらかじめ入力されます。省略可能なパラメーターは自動的には要求されないため、値がないとインテントを続行できない場合は、perform() で値を要求してください。

// Required; the system asks for a value when needed
@Parameter(title: "Count")
var count: Int

// Required and pre-filled
@Parameter(title: "Count", default: 1)
var count: Int

// Optional; request it yourself if it becomes necessary
@Parameter(title: "Count")
var count: Int?
対応する値の型

プリミティブ型:Bool、Int、Double、String、Duration、Date、Decimal、 Measurement、URL。コレクション:対応する要素型の Array と Set。フレームワーク:IntentPerson、IntentFile。カスタム型:任意の AppEntity または AppEnum。

よく使うイニシャライザーのパターン
// Basic
@Parameter(title: "Name")
var name: String

// With default
@Parameter(title: "Count", default: 5)
var count: Int

// Numeric slider
@Parameter(title: "Volume", controlStyle: .slider, inclusiveRange: (0, 100))
var volume: Int

// Options provider (dynamic list)
@Parameter(title: "Category", optionsProvider: CategoryOptionsProvider())
var category: Category

// File with content types
@Parameter(title: "Document", supportedContentTypes: [.pdf, .plainText])
var document: IntentFile

// Measurement with unit
@Parameter(title: "Distance", defaultUnit: .miles, supportsNegativeNumbers: false)
var distance: Measurement<UnitLength>

すべてのイニシャライザーのバリエーションについては、references/appintents-advanced.md を参照してください。

AppEntity

アプリのデータを反映し、システム向けのフィールドのみを公開するシャドウモデルを優先してください。 モデルが軽量で安定しており、App Intents のライフサイクルに適している場合は、モデルを直接準拠させてもかまいません。

struct SoupEntity: AppEntity {
    static let defaultQuery = SoupEntityQuery()
    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Soup"
    var id: String

    @Property(title: "Name") var name: String
    @Property(title: "Price") var price: Double

    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "\(name)", subtitle: "$\(String(format: "%.2f", price))")
    }

    init(from soup: Soup) {
        self.id = soup.id; self.name = soup.name; self.price = soup.price
    }
}

必須:id、defaultQuery(static)、displayRepresentation、 typeDisplayRepresentation(static)。フィルタリングや並べ替えに公開するプロパティには、@Property(title:) を付けてください。@Property のないプロパティは内部用のままです。

EntityQuery(4 種類)

1. EntityQuery(基本 -- ID で解決)
struct SoupEntityQuery: EntityQuery {
    func entities(for identifiers: [String]) async throws -> [SoupEntity] {
        SoupStore.shared.soups.filter { identifiers.contains($0.id) }.map { SoupEntity(from: $0) }
    }
    func suggestedEntities() async throws -> [SoupEntity] {
        SoupStore.shared.featured.map { SoupEntity(from: $0) }
    }
}
2. EntityStringQuery(フリーテキスト検索)
struct SoupStringQuery: EntityStringQuery {
    func entities(matching string: String) async throws -> [SoupEntity] {
        SoupStore.shared.search(string).map { SoupEntity(from: $0) }
    }
    func entities(for identifiers: [String]) async throws -> [SoupEntity] {
        SoupStore.shared.soups.filter { identifiers.contains($0.id) }.map { SoupEntity(from: $0) }
    }
}
3. EnumerableEntityQuery(有限の集合)
struct AllSoupsQuery: EnumerableEntityQuery {
    func allEntities() async throws -> [SoupEntity] {
        SoupStore.shared.allSoups.map { SoupEntity(from: $0) }
    }
    func entities(for identifiers: [String]) async throws -> [SoupEntity] {
        SoupStore.shared.soups.filter { identifiers.contains($0.id) }.map { SoupEntity(from: $0) }
    }
}
4. UniqueAppEntityQuery(シングルトン、iOS 18+)

アプリの設定など、インスタンスが 1 つだけのエンティティに使用してください。

struct AppSettingsEntity: UniqueAppEntity {
    static let defaultQuery = AppSettingsQuery()
    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Settings"
    var displayRepresentation: DisplayRepresentation { "App Settings" }

    var id: String { "app-settings" }
}

struct AppSettingsQuery: UniqueAppEntityQuery {
    func uniqueEntity() async throws -> AppSettingsEntity {
        AppSettingsEntity()
    }
}

フィルタリング/並べ替えをサポートする EntityPropertyQuery については、references/appintents-advanced.md を参照してください。

あるパラメーターによって別のパラメーターで有効なエンティティの選択肢が決まる場合は、クエリまたはオプションプロバイダーで @IntentParameterDependency を使用してください。詳細なリファレンスでは、上流の値が nil の場合と、意図的なデフォルト値について説明しています。

AppEnum

選択可能な値の固定セットを定義します。RawValue は LosslessStringConvertible に準拠している必要があります。読みやすく安定した識別子にするため、String の raw value を優先してください。

enum SoupSize: String, AppEnum {
    case small, medium, large

    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Size"

    static var caseDisplayRepresentations: [SoupSize: DisplayRepresentation] = [
        .small: "Small",
        .medium: "Medium",
        .large: "Large"
    ]
}
// Valid, but less readable in saved shortcuts and URL representations
enum Priority: Int, AppEnum {
    case low = 1, medium = 2, high = 3
}

// Preferred
enum Priority: String, AppEnum {
    case low, medium, high
    // ...
}

AppShortcutsProvider

ユーザーによる設定なしで Siri とショートカットアプリに表示される、事前定義済みのショートカットを登録します。

struct MyAppShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: OrderSoupIntent(),
            phrases: [
                "Order \(\.$soup) in \(.applicationName)",
                "Get soup from \(.applicationName)"
            ],
            shortTitle: "Order Soup",
            systemImageName: "cup.and.saucer"
        )
    }

    static var shortcutTileColor: ShortcutTileColor = .navy
}
フレーズのルール
  • すべてのフレーズに \(.applicationName) を含める必要があります。
  • フレーズではパラメーターを参照できます: \(\.$soup)。
  • 動的なオプション値が変わったときは、updateAppShortcutParameters() を呼び出してください。
  • Siri の誤起動を防ぐには、negativePhrases を使用してください。

システムサーフェスとの統合

Siri の提案/予測、インタラクティブなウィジェット、コントロールセンターの設定とアクションの分離、または Spotlight の IndexedEntity インデックス作成を実装する場合は、references/system-surfaces.md をお読みください。

iOS 26 の追加機能

SnippetIntent

システム UI にインタラクティブなスニペットを表示します:

struct OrderStatusSnippet: SnippetIntent {
    static var title: LocalizedStringResource = "Order Status"
    func perform() async throws -> some IntentResult & ShowsSnippetView {
        let status = await OrderTracker.currentStatus()
        return .result(view: OrderStatusSnippetView(status: status))
    }
}

struct CheckOrderStatusIntent: AppIntent {
    static var title: LocalizedStringResource = "Check Order Status"
    func perform() async throws -> some IntentResult & ShowsSnippetIntent {
        .result(snippetIntent: OrderStatusSnippet())
    }
}

システムは、スニペットのボタンやトグルの操作後などに perform() を複数回呼び出すことがあります。SnippetIntent.perform() は副作用のない処理にし、変更は呼び出し元のアクションインテント、または別のボタン/トグルアクションで行ってください。スニペット専用のインテントは、isDiscoverable が true でない限り、ショートカットや Spotlight から見つけることはできません。

IntentValueQuery(ビジュアルインテリジェンス)
@available(iOS 26, *)
@UnionValue
enum ShoppingVisualResult {
    case product(ProductEntity)
    case store(StoreEntity)
}

@available(iOS 26, *)
struct ShoppingVisualQuery: IntentValueQuery {
    func values(for input: SemanticContentDescriptor) async throws -> [ShoppingVisualResult] {
        try Task.checkCancellation()
        async let productMatches = ProductStore.shared.matches(
            labels: input.labels,
            pixelBuffer: input.pixelBuffer,
            limit: 5
        )
        async let storeMatches = StoreStore.shared.matches(
            labels: input.labels,
            pixelBuffer: input.pixelBuffer,
            limit: 3
        )
        let ranked = await rank(productMatches, storeMatches)
        return Array(ranked.prefix(8))
    }
}

SemanticContentDescriptor を受け取れる IntentValueQuery は 1 つだけです。1 つのクエリで複数のアプリエンティティ型を返す必要がある場合は、@UnionValue を使用してください。labels は、網羅的な同義語やアプリ固有の分類ではなく、上位概念を示す英語の記述として扱ってください。利用可能な場合は、pixelBuffer と組み合わせてください。結果は少数に絞って順位付けし、キャンセルに対応させてください。また、詳細情報や追加の結果を得るために、OpenIntent、URL 表現、またはアプリ内検索への引き継ぎを用意してください。App Intents のクエリ内では、カメラ撮影、Vision の VN* リクエスト、バーコード分類、Spotlight のインデックス作成を実装しないでください。代わりに、既存の処理範囲が限定されたアプリ内検索または画像照合サービスを呼び出し、システム UI の処理時間を超える可能性がある場合は、結果数とタイムアウトを明示的に制限してください。

よくある間違い

  1. AppEntity を通じてアプリのモデル状態を公開しすぎる。 安定した永続 ID とシステム向けのプロパティのみを持つ専用のシャドウモデルを使用してください。

  2. フレーズに \(.applicationName) を含め忘れる。 すべての AppShortcut のフレーズには、アプリケーション名のトークンを含める必要があります。Siri は曖昧さを解消するためにこれを使用します。

  3. オプションの @Parameter を必須として扱う。 オプションのパラメーターは自動的には要求されません。インテントを続行するために値が必要な場合は、requestValue / needsValueError を呼び出してください。

    // Optional, so request it yourself if needed
    @Parameter(title: "Count")
    var count: Int?
    
  4. 不安定な AppEnum の raw value を使用する。 Int も有効ですが、永続化や URL 表現には通常、String の raw value のほうが明確です。

  5. suggestedEntities() を忘れる。 これがないと、ショートカットのピッカーにデフォルト項目が表示されません。

  6. entities(for:) でエンティティが見つからない場合にエラーをスローする。 代わりに、見つからないエンティティは除外してください。

  7. Spotlight のインデックスが古い。 名前付きの CSSearchableIndex でエンティティを再インデックスしてください。

  8. typeDisplayRepresentation がない。 AppEntity と AppEnum の両方で必要です。

  9. 非推奨の @Assistant* スキーママクロを使用する。 @AppIntent(schema:)、@AppEntity(schema:)、@AppEnum(schema:) を使用してください。

  10. perform() をブロックさせたり、副作用を発生させたりする。 I/O には await を使用してください。システムが再実行する可能性があるため、SnippetIntent.perform() は副作用のない処理にしてください。

  11. ガードなしでシステムの画面から機密性の高い状態を変更する。 ドアロック、照明、購入、削除などの操作には、確認や認証を使用してください。

レビューチェックリスト

  • すべての AppIntent に説明的な title(動詞+名詞、タイトルケース)がある
  • 必須の @Parameter の値はオプションではない。オプションの値は必要に応じて要求される
  • AppEntity 型は安定した ID とシステム向けのプロパティのみを公開する
  • AppEntity に displayRepresentation と typeDisplayRepresentation がある
  • EntityQuery.entities(for:) は見つからない ID を除外し、suggestedEntities() が実装されている
  • 依存するオプションには @IntentParameterDependency を使用し、本当に役立つデフォルトがある場合に限り defaultResult() を実装する
  • AppEnum では、caseDisplayRepresentations を備えた安定した String の raw value を優先する
  • AppShortcutsProvider のフレーズに \(.applicationName) が含まれ、parameterSummary が定義されている
  • IndexedEntity のプロパティでキーパスの indexingKey 値を使用し、エンティティがインデックス登録されている
  • Control Center のインテントは ControlConfigurationIntent に、ウィジェットのインテントは WidgetConfigurationIntent に準拠している。デフォルト値のないコントロールパラメーターはオプションである
  • 機密性の高い App Intents は、状態を変更する前に確認や認証を要求する
  • Visual Intelligence の IntentValueQuery は SemanticContentDescriptor、上限付きの結果、オープニングパス、および iOS 26 の利用可能性を使用する
  • 非推奨の @AssistantIntent / @AssistantEntity / @AssistantEnum スキーママクロを使用していない
  • perform() は async/await を使用する(ブロックしない)。想定される分離コンテキストで実行され、インテント型は Sendable である

参考資料

  • Siri、ウィジェット、Control Center、Spotlight の統合パターンについては、references/system-surfaces.md をお読みください。
  • @Parameter の各種、EntityPropertyQuery、アシスタントスキーマ、集中モードフィルター、SiriKit の移行、エラー処理、確認フロー、認証、URL 表現可能な型、Spotlight のインデックス登録の詳細については、references/appintents-advanced.md をご覧ください。

コメント