App IntentのSnippetによるインタラクティブなUIの実現
wwdc2025で触れられていたApp Intentの機能強化のうちSnippet Intentが気になっていたので、サンプルアプリに導入してみつつ記事にしました。
AppIntentについて
AppIntentは、アプリの提供機能をショートカットやSiriなどのシステムに公開するためのフレームワークです。
iOS16以降で利用可能になっています。

本記事で使用する主な要素:
| 要素 | 説明 |
|---|---|
| AppIntent | アプリのアクションを定義するプロトコル |
| AppEntity | アプリのデータをシステムに公開するための型 Intent間でデータを受け渡す際に使用する |
| SnippetIntent | インタラクティブなビューを表示するIntent(iOS 26+) |
サンプルアプリ
行った運動を記録し、サマリー表示できるアプリ。
- 主な機能: 運動の開始・終了、今日のサマリー表示
- 運動の記録(種目や時間、消費カロリーなど)はSwiftDataを利用して情報を保持
アプリ本体のイメージ

AppIntentを利用したコンテンツ表示
アプリ本体で提供している一部の機能をApp IntentやSnippet Intentを利用して、ショートカットやSiri等のシステムから取り扱えるようにしてみました。
今日の運動サマリーを表示する

実装
GetExerciseSummaryIntentで、今日の運動サマリーを表示するシンプルなIntentを実装しています。perform()メソッドでは、ExerciseSummaryView(SwiftUIView)という運動サマリーを表示するビューを返しています。
struct GetExerciseSummaryIntent: AppIntent {
static var title: LocalizedStringResource = "今日の運動を表示"
@MainActor
func perform() async throws -> some IntentResult & ShowsSnippetView {
let latest = try resolveExercise()
let todayEntries = try await fetchTodayEntries()
return .result {
ExerciseSummaryView(
latestEntry: latest,
todayEntries: todayEntries
)
}
}
}
運動の開始・終了
※動画はショートカット登録されたIntentをSiri経由で実行したものです

実装
iOS 26以上で対応のSnippetIntentを使うと、ボタンやトグルを含むインタラクティブなコンテンツを表示できます。
フロー図
StartExerciseIntent
システムから呼ばれるAppIntentです。ShowsSnippetIntentを返し、ActiveExerciseSnippetIntentに処理を委譲します。
-
.result(snippetIntent:)でSnippetIntentに準拠したActiveExerciseSnippetIntentを返しています -
ActiveExerciseSnippetIntentには新しい運動記録を表しているExerciseEntryEntity(AppEntityに準拠)を渡しています
struct StartExerciseIntent: AppIntent {
static let title: LocalizedStringResource = "運動を開始"
@Parameter(title: "運動種別")
var exerciseType: ExerciseType
func perform() async throws -> some IntentResult & ShowsSnippetIntent {
// SwiftDataに新しい運動記録を追加
let container = try ModelContainer(for: ExerciseEntry.self)
let context = ModelContext(container)
let newExercise = ExerciseEntry(exerciseType: exerciseType, duration: 0)
// isActiveは運動を開始している状態を表しています
newExercise.isActive = true
context.insert(newExercise)
try context.save()
// SnippetIntentに処理を委譲
return .result(
snippetIntent: ActiveExerciseSnippetIntent(exercise: newExercise.toEntity())
)
}
}
ActiveExerciseSnippetIntent
-
SnippetIntentプロトコルに準拠したIntentです。 -
perform()の戻りにShowsSnippetViewを指定し、ビューを返しています。
struct ActiveExerciseSnippetIntent: SnippetIntent {
static let title: LocalizedStringResource = "Active Exercise Request Snippet"
@Parameter var exercise: ExerciseEntryEntity
func perform() async throws -> some IntentResult & ShowsSnippetView {
return .result(view: ActiveExerciseView(exercise: exercise))
}
}
ActiveExerciseView
ActiveExerciseViewの実装内容です。
先程のStartExerciseIntentから渡された運動記録の状態に応じて、表示するViewを切り替えています。
struct ActiveExerciseView: View {
let exercise: ExerciseEntryEntity
var body: some View {
if exercise.isActive {
// 開始している運動がある場合のビュー
ActiveExerciseContentView(exercise: exercise)
} else {
// 運動が完了した状態のビュー
CompletedExerciseView(exercise: exercise)
}
}
}
開始している運動がある場合のビューの実装内容です。
Snippet内でボタンを使うには、Button(intent:)を使用します。
ボタン押下により、StopExerciseIntentというIntentが実行されます。
struct ActiveExerciseContentView: View {
let exercise: ExerciseEntryEntity
var body: some View {
VStack(spacing: 16) {
// 運動情報の表示
HStack {
Image(systemName: exercise.exerciseType.iconName)
Text(exercise.exerciseType.rawValue)
}
// 運動終了ボタン
Button(intent: StopExerciseIntent(exerciseId: exercise.id)) {
HStack {
Image(systemName: "stop.circle.fill")
Text("運動を終了")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.red)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
.buttonStyle(.plain)
}
.padding()
}
}
StopExerciseIntent
アクティブな運動を終了するためのIntentです。
iOS26+で利用可能なUndoableIntentに準拠しています。
perform()ではisActiveをfalse(運動を終了)にしたentityを返しています。
これにより、システムがActiveExerciseSnippetIntent.perform()を再実行し、最新の状態が反映されたビューが表示されるようになります。
struct StopExerciseIntent: UndoableIntent {
static let title: LocalizedStringResource = "運動を終了"
@Parameter(title: "運動ID")
var exerciseId: String
func perform() async throws -> some IntentResult & ReturnsValue<ExerciseEntryEntity> {
// 運動を終了する処理
exercise.isActive = false
exercise.duration = elapsedMinutes
try context.save()
return .result(value: exercise.toEntity())
}
}
AppShortcutsProvider
エントリーポイントとなるGetExerciseSummaryIntent,StartExerciseIntentをショートカットアプリから呼び出せるようにしたAppShortcutsProviderの実装です。
phrasesに、Siriがショートカットを実行するためのトリガーとなる音声フレーズを指定しています。
struct AppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
// 運動を開始
AppShortcut(
intent: StartExerciseIntent(),
phrases: [
"\(.applicationName)で運動を開始",
"\(.applicationName)で運動を始める"
],
shortTitle: "運動を開始",
systemImageName: "figure.run"
)
// 今日の運動を表示
AppShortcut(
intent: GetExerciseSummaryIntent(),
phrases: [
"\(.applicationName)で今日の運動を見せて",
"\(.applicationName)の運動記録"
],
shortTitle: "今日の運動",
systemImageName: "list.bullet.clipboard"
)
}
}
まとめ
今回、はじめてAppIntentを扱ってみました。
アプリを起動しなくても、アプリが提供する機能をショートカット・Siriなどのシステム経由で利用できるという点で、ユーザーに提供できる価値の幅も広がりそうです。
また、iOS26で対応されたSnippetIntentにより、通常のアプリに近い感覚で動的なコンテンツを実装できそうな点も良いですね。
もう少し深く学習し、個人開発や自社のプロダクトへの導入も検討していきたいです!
記事で触れたサンプルアプリは下記リポジトリで公開しています。
Discussion