axiom-app-intents-ref について
AppleのApp Intentsフレームワークを使い、アプリの操作やコンテンツをシステム機能に公開するためのSwift開発者向けガイドです。インテントやエンティティの設計から、Siri、ショートカット、ウィジェット、Visual Intelligenceとの連携まで扱います。
主な機能
- 01アクション、エンティティ、列挙型の定義
- 02パラメータとエンティティクエリの設定
- 03実行方法と認証の制御
- 04確認と選択のフロー追加
- 05インタラクティブなスニペットの返却
- 06Appleのシステム機能との連携
活用シーン
- 01アプリの操作をSiriやショートカットに公開
- 02Spotlightでアプリのコンテンツを検索可能にする
- 03Visual Intelligenceの結果クエリを実装
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-app-intents-refインストール後、Claude Code で/axiom-app-intents-refを入力して読み込まれたか確認します。
詳細
App Intents の統合
概要
Siri、Apple Intelligence、ショートカット、Spotlight、その他のシステム機能にアプリの機能を公開するための App Intents フレームワークについて、包括的に説明します。従来の SiriKit カスタムインテントを、最新の Swift ファーストな API に置き換えます。
基本原則 App Intents により、Apple のエコシステム全体でアプリのアクションを見つけられるようになります。適切に設計されたインテントは、Siri との会話、ショートカットの自動化、Spotlight 検索で自然に使えます。
このスキルを使用する場面
- アプリの機能を Siri や Apple Intelligence に公開する
- アプリのアクションをショートカットアプリで利用できるようにする
- アプリのコンテンツを Spotlight で検索できるようにする
- 集中モードのフィルタ、ウィジェット、ライブアクティビティと連携する
- アクションボタン(Apple Watch Ultra)に対応する
- インテントの解決やパラメーターの検証に失敗する問題をデバッグする
- ショートカットアプリでインテントをテストする
- アプリのコンテンツに対するエンティティクエリを実装する
関連スキル
- app-shortcuts-ref — ユーザーによる設定なしで Siri/Spotlight からすぐに利用できる App Shortcuts
- core-spotlight-ref — コンテンツのインデックス作成のための Core Spotlight と NSUserActivity の統合
- app-discoverability — あらゆる API を活用し、システム全体でアプリを表示するための戦略ガイド
対応するシステム機能
App Intents は次の機能と統合されます。
- Siri — 音声コマンドと Apple Intelligence
- ショートカット — 自動化ワークフロー
- App Shortcuts — 事前設定済みのアクションをすぐに利用可能(app-shortcuts-ref を参照)
- Spotlight — 検索による発見
- 集中モードのフィルタ — コンテキストに応じたフィルタリング
- アクションボタン — クイックアクション(Apple Watch Ultra)
- コントロールセンター — カスタムコントロール
- WidgetKit — インタラクティブなウィジェット
- ライブアクティビティ — Dynamic Island の更新
- Visual Intelligence — 画像を使ったインタラクション
Visual Intelligence との統合
IntentValueQuery
ユーザーが Visual Intelligence のカメラでオブジェクトを囲むと、アプリから一致する結果を表示できるようにします。
@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)
}
}
}
基本概念
3 つの構成要素
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
}
バックグラウンド実行とフォアグラウンド実行
インテントのモード
ブール値の openAppWhenRun の代わりに supportedModes を使用して、実行コンテキストを細かく制御します。
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
App Intents を Swift Package および静的ライブラリに含めます:
// 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]
}
}
これにより、パッケージ境界を越えたモジュール式のインテント定義が可能になります。アプリターゲットは includedPackages を介してすべてのパッケージを集約します。
Apple Intelligence:Use Model アクション
概要
ショートカットの Use Model アクション(iOS 18.1 以降)を使用すると、ユーザーは Apple Intelligence モデルを自動化ワークフローに組み込むことができます。アプリのエンティティを言語モデルに渡して、フィルタリング、変換、推論を行うことができます。
主な機能 内部的には、このアクションはエンティティの JSON 表現をモデルに渡すため、モデルが推論できるようにしたい情報はすべてエンティティ定義で公開するようにしてください。
3 つの出力タイプ
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 によるリッチテキストのサポート
なぜ重要か アプリがリッチテキストコンテンツをサポートしているなら、今こそ、アプリインテントのテキストパラメータに適切な箇所でアトリビューテッド文字列型を使用するようにしてください。
❌ しない:プレーンな 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 アプリの Create Note は AttributedString を受け入れるため、ChatGPT からの日記テンプレートに以下を含めることができます:
- 太字の見出し
- 気分記録の表
- 書式設定されたリスト
- すべてロスレスに保持されます
自動型変換
Use Model の出力が別のアクションに接続されると、ランタイムが自動的に型を変換します:
例:If アクション用の Boolean
// 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
「はい、このノートは Shortcuts アプリの機能開発に関するもののようです」のような冗長なテキストを返す代わりに、If アクションに接続されると、モデルは自動的に Boolean(true/false)を返します。
利用可能な明示的な出力タイプ
- テキスト(AttributedString)
- 数値
- Boolean
- 辞書
- 日付
- アプリエンティティ
フォローアップ機能
次のアクションに渡す前に反復的な改善を可能にします:
// 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を採用すると、次の機能を利用できます。
- ショートカットでの検索アクションの自動生成
- プロパティに基づく絞り込み
- 検索のサポート
- 必要なコードを最小限に削減
基本的な実装
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に準拠すると、ユーザーは次の検索アクションを自動的に利用できます。
ショートカットアプリの場合
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以降)では、システム検索からアプリのインテントを直接実行できます。適切に設定すれば、ショートカットで動作するインテントはSpotlightでも自動的に動作します。
重要な原則 Spotlightでは、操作をすばやく実行できることが重要です。そのためには、インテントの実行に必要な情報をすべて、Spotlight内で直接入力できるようにする必要があります。
Spotlightに表示するための要件
1. パラメーターの概要にすべての必須パラメーターを含める
SpotlightのUIに表示されるパラメーターの概要には、デフォルト値のない必須パラメーターをすべて含める必要があります。
❌ 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. インテントを非表示にしない
ショートカットで非表示にしたインテントは、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
}
画面上のコンテンツのタグ付けについて詳しくは、「Exploring New Advances in 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()
}
}
ユーザー体験
- ユーザーがSpotlightで「イベントを作成」を実行する(バックグラウンド)
- アプリを開かずにイベントが作成される
- Spotlightに「アプリで開く」ボタンが表示される(opensIntent)
- ユーザーがボタンをタップすると、アプリがイベントの詳細画面を開く
予測可能なインテントプロトコル
使用パターンに基づく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 から)
- Bluetooth(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 は、一般的なアプリカテゴリ向けのビルトインスキーマを提供しています。
Books アプリの例
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 — ファイルの管理、ページの操作、検索
テストとデバッグ
ショートカットアプリでのテスト
-
ショートカットにインテントを追加する:
- ショートカットアプリを開く
- 「+」をタップして新しいショートカットを作成する
- アプリ名を検索する
- インテントを選択する
-
パラメーター解決をテストする:
- パラメーターを入力する
- ショートカットを実行する
- Xcode コンソールでログを確認する
-
Siri でテストする:
- 「Hey Siri、[your intent name]」
- 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 の音声コマンドでテスト済み
- ☐ ショートカットアプリでテスト済み
- ☐ さまざまなパラメーターの組み合わせでテスト済み
- ☐ サポートされているすべての言語でローカライズを確認済み
リソース
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、ショートカット、システム機能を通じてユーザーがアプリを操作するための仕組みです。適切に設計されたインテントは、アプリの機能を自然に拡張し、Apple のエコシステム全体で価値を提供します。