WWDC Quick Look 💓 By SwiftGGTeam
Get to know App Intents

Get to know App Intents

元の動画を見る

Highlight

本セッションは App Intents フレームワークのゼロからの入門編です。intent、entity、query という 3 つのコアコンセプトから始まり、App Shortcut、Spotlight インデックス、SwiftUI ナビゲーションとの統合といった応用までを一気通貫で解説します。スピーカーはランドマーク閲覧 app を題材にした demo を一貫して用い、Spotlight、Siri、Action Button から呼び出せる完全な intent 体系を段階的に組み上げていきます。


コアコンテンツ

多くの app には次のような悩みがあります。ユーザーは特定のページに飛びたいだけなのに、まず app を開き、メニューを何階層もたどらなければなりません。Siri に話しかける、Spotlight で検索する、Action Button を押す——それだけで目的のページに直行できれば理想的ですが、その間には app の外側にあるシステム機能の層が存在します。App Intents はこのギャップを埋めるために Apple が用意したフレームワークです。

スピーカーの James は、ランドマーク app を題材に解説を進めます。app には Landmarks 一覧、Map、Collections の 3 つの tab があります。彼はまず 20 行ほどの Swift コードで「Landmarks ページに遷移する」処理を NavigateIntent としてラップしました。app をインストールすると Shortcuts からすぐにこのアクションが見えるようになり、さらに static let supportedModes: IntentModes = .foreground を 1 行追加することで、実行時に app が自動的に foreground に持ち上げられるようになります。続いて遷移先を NavigationOption という AppEnum に抽出し、@Parameter でユーザーに目的地を選ばせるようにしたうえで、AppShortcutsProvider を追加することで、Siri、Spotlight、Action Button 上で同時にこの機能が利用可能になります。

このフレームワークの設計思想は、app の機能を「動詞」(intent)と「名詞」(entity)に抽象化し、query を介してシステムにあなたのデータを理解させることにあります。今年のアップデートは主に 4 点に集約されます。entity プロパティが @ComputedProperty をサポートし、既存 model から遅延的に値を取得できるようになったことでコピーが不要に。Spotlight が @Property 上に indexingKey を直接アノテートできるように。新しい TargetContentProvidingIntent プロトコルと onAppIntentExecution modifier の組み合わせによる宣言的なナビゲーション。そして App Intents がついに Swift Package や static library 内での型定義をサポートし、AppIntentsPackage で登録することで target をまたいで共有可能になりました。


詳細

App Intents の最小単位は AppIntent プロトコルを実装する struct で、必須要素は 2 つだけです。titleperform() メソッドです。以下は demo に最初に登場する NavigateIntent です(03:23)。

struct NavigateIntent: AppIntent {
    static let title: LocalizedStringResource = "Navigate to Landmarks"

    static let supportedModes: IntentModes = .foreground

    @MainActor
    func perform() async throws -> some IntentResult {
        Navigator.shared.navigate(to: .landmarks)
        return .result()
    }
}

ポイント:

  • title は定数文字列でなければなりません。フレームワークがコンパイル時にソースコードを読み取ってメタデータを生成するため、計算プロパティは利用できません。
  • supportedModes = .foreground を指定すると、intent 実行前に app が foreground に持ち上げられます。デフォルトはバックグラウンド実行です。
  • @MainActor により perform がメインスレッドで実行されることが保証されます。ナビゲーションはメインスレッド上で行う必要があるためです。
  • 戻り値の IntentResult には dialog(Siri の読み上げ)、view snippet(システムのポップアップ内に描画)、ReturnsValue(次の intent への入力として渡せる値)を持たせることができます。

固定ページに飛ぶだけでは柔軟性に欠けます。次のステップは遷移先をパラメータ化することです(05:38)。

struct NavigateIntent: AppIntent {
    static let title: LocalizedStringResource = "Navigate to Section"

    static let supportedModes: IntentModes = .foreground

    static var parameterSummary: some ParameterSummary {
        Summary("Navigate to \(\.$navigationOption)")
    }

    @Parameter(
        title: "Section",
        requestValueDialog: "Which section?"
    )
    var navigationOption: NavigationOption

    @MainActor
    func perform() async throws -> some IntentResult {
        Navigator.shared.navigate(to: navigationOption)
        return .result()
    }
}

ポイント:

  • @Parameter は変数を intent の入力として宣言します。Optional を付けない場合は必須となり、ランタイムにユーザーへ値の入力を要求してから perform を呼び出します。
  • parameterSummarySummary("Navigate to \(\.$navigationOption)") のように記述すると、アクションとパラメータが自然な一文に組み合わさります。Shortcuts ではパラメータがタップ可能なインライン UI として描画されます。
  • requestValueDialog は Siri がユーザーに値を尋ねる際に読み上げるプロンプトです。
  • 今年からの新ルールとして、intent が完全な parameter summary を実装していれば、macOS Spotlight から直接実行できるようになりました(08:09)。

次のステップは、app の中で本当に動的な「名詞」をモデリングすることです。Landmark の数は固定ではないため、AppEntity を使う必要があります。今年新たに加わった @ComputedProperty を使うと、データを model から entity へコピーする必要がなくなります(11:02)。

struct LandmarkEntity: AppEntity {
    var id: Int { landmark.id }

    @ComputedProperty
    var name: String { landmark.name }

    @ComputedProperty
    var description: String { landmark.description }

    let landmark: Landmark

    static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Landmark")

    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "\(name)")
    }

    static let defaultQuery = LandmarkEntityQuery()
}

ポイント:

  • id は永続化可能で、データベース検索に利用できる識別子でなければなりません。システムはこの id をキャッシュしており、再起動後も元の entity に解決できます。
  • @ComputedProperty は今年の新機能で、getter と等価です。Shortcuts がこれらのフィールドを読むタイミングで初めて model にアクセスするため、データの二重保持を避けられます。
  • defaultQuery は entity と EntityQuery を結びつけ、システムがこの query を介して「この id に対応する entity は何か」といった問い合わせに答えます。

Query は entity の「住所」のようなものです。最も基本的な実装は「この ID に対応する entity は何か」を答える役割を担います(13:19)。

struct LandmarkEntityQuery: EntityQuery {
    @Dependency var modelData: ModelData

    func entities(for identifiers: [LandmarkEntity.ID]) async throws -> [LandmarkEntity] {
        modelData
            .landmarks(for: identifiers)
            .map(LandmarkEntity.init)
    }
}

ポイント:

  • @Dependency は外部のデータソースを query にインジェクションします。app の起動初期に AppDependencyManager.shared.add { ModelData() } で登録しておく必要があります。
  • entities(for:) はすべての query が実装しなければならないメソッドで、システムは id のリストを渡してこのメソッドから entity インスタンスを解決します。
  • これに加えて EnumerableEntityQuery(全件返却)、EntityPropertyQuery(述語ソート対応)、EntityStringQuery(文字列マッチ)の 3 つのプロトコルを拡張可能で、それぞれ Shortcuts における Find、Filter、Search の挙動に対応します。

最後はナビゲーションの宣言的な記述方法です。今年追加された TargetContentProvidingIntentperform メソッドを必要とせず、ナビゲーションロジックを SwiftUI view に直接アタッチします(18:17)。

struct OpenLandmarkIntent: OpenIntent, TargetContentProvidingIntent {
    static let title: LocalizedStringResource = "Open Landmark"

    @Parameter(title: "Landmark", requestValueDialog: "Which landmark?")
    var target: LandmarkEntity
}

struct LandmarksNavigationStack: View {
    @State var path: [Landmark] = []

    var body: some View {
        NavigationStack(path: $path) {}
        .onAppIntentExecution(OpenLandmarkIntent.self) { intent in
            path.append(intent.target.landmark)
        }
    }
}

ポイント:

  • OpenIntent プロトコルは target という名前のパラメータを要求し、perform の前に自動的に app を foreground に持ち上げます。
  • TargetContentProvidingIntent を使うと perform の実装をスキップでき、「パラメータを受け取った後に何をするか」を view 層に移譲できます。
  • onAppIntentExecution(...) modifier は対応する intent をリッスンし、クロージャ内で SwiftUI のナビゲーションパスを直接変更します。これにより、従来のグローバルな Navigator シングルトンといった糊付けコードが不要になります。

コアの示唆

  • 何をやるか: 既存の app に最小構成の AppShortcut を追加し、「よく使うページを開く」操作を Spotlight と Siri に公開する。

    • なぜやる価値があるか: Session で示されたコストから見ても、わずか数十行の Swift で済みます。それでいてユーザー体験は即座に向上します。app を開いてからホームを経由して tab をタップする必要がなくなり、Action Button への直接バインドも可能になります。
    • どこから始めるか: 03:23 の NavigateIntent を参考に、まずパラメータなしのバージョンを書き、AppShortcutsProvider にフレーズを 1 つ追加します。インストール後すぐに Shortcuts で動作を確認できます。
  • 何をやるか: app 内に頻出する「ドメインオブジェクト」(注文、ノート、お気に入り、場所など)を AppEntity としてモデリングし、IndexedEntity を実装する。

    • なぜやる価値があるか: IndexedEntity を実装するとシステムが自動的に entity を Spotlight インデックスに書き込み、セマンティック検索もサポートしてくれます。ユーザーはロック画面を下にスワイプするだけであなたの app のコンテンツを検索でき、競争力が一気にシステムレベルまで引き上がります。
    • どこから始めるか: まず @ComputedProperty で既存 model のフィールドをラップして(データの二重書き込みを回避)、重要なプロパティに @Property(indexingKey:) を付与し、最後に CSSearchableIndex の donate メソッドを呼び出します。
  • 何をやるか: app 内部のナビゲーションロジックを、グローバルなシングルトンから TargetContentProvidingIntent + onAppIntentExecution の組み合わせへ移行する。

    • なぜやる価値があるか: この宣言的なスタイルにより、deep link、Spotlight タップ、Siri 音声という 3 つの入口で同じナビゲーションコードを共有できるようになり、手書きの router が不要になります。同時に SwiftUI の状態の可観測性もそのまま維持されます。
    • どこから始めるか: まずは最も頻繁に使う「詳細ページを開く」シナリオを 1 つ選び、OpenXxxIntent を定義して両方のプロトコルを実装します。NavigationStack に onAppIntentExecution を取り付け、クロージャ内で path に append すれば完了です。
  • 何をやるか: entity に Transferable を実装して、画像やドキュメントなどの代表的なコンテンツを Shortcuts や他の app に公開する。

    • なぜやる価値があるか: ユーザーはあなたの entity をシステムの写真、メッセージ、Mail などのアクションへ直接渡せるようになり、複数ステップの Shortcut も自動でつながるようになります。これは無料で得られるクロス app 連携機能です。
    • どこから始めるか: 16:33 の DataRepresentation(exportedContentType: .image) を参考に、entity 内部にすでに存在する画像データを宣言するだけで OK です。
  • 何をやるか: App Intents の型を Swift Package に配置し、AppIntentsPackage プロトコルと組み合わせて target をまたいで共有する。

    • なぜやる価値があるか: 今年から package と static library でも intent を定義できるようになり、コアドメインモデルと UI を分離しやすくなりました。複数 target のプロジェクト(メイン app + extension + widget)で同じ entity を共用でき、重複定義を避けられます。
    • どこから始めるか: entity ファイルを SPM の target に移動し、struct XxxKitPackage: AppIntentsPackage {} を定義します。app target の package では includedPackages 経由で取り込みます。

関連 Session

コメント

GitHub Issues · utterances