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 つだけです。title と perform() メソッドです。以下は 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 を呼び出します。parameterSummaryでSummary("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 の挙動に対応します。
最後はナビゲーションの宣言的な記述方法です。今年追加された TargetContentProvidingIntent は perform メソッドを必要とせず、ナビゲーションロジックを 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
- Design interactive snippets — App Intent と組み合わせて使う snippet view のデザインガイドです。perform の結果をシステム上で実用的かつ見栄え良くレンダリングする方法を解説しています。
- Code-along: Bring on-device AI to your app using the Foundation Models framework — オンデバイス LLM を SwiftUI app に組み込むコード実装の実演で、App Intent はその代表的な呼び出し口となります。
- Deep dive into the Foundation Models framework — Foundation Models の応用編です。guided generation とツール呼び出しは App Intent のコンセプトと相通じる部分があります。
- Bring advanced speech-to-text to your app with SpeechAnalyzer — 新しい SpeechAnalyzer API です。App Intent と組み合わせて、音声からアクションへつなげるパイプラインを構成するケースが多く見られます。
コメント
GitHub Issues · utterances