WWDC Quick Look 💓 By SwiftGGTeam
App Intents フレームワークの新機能を知る

App Intents フレームワークの新機能を知る

元の動画を見る

ハイライト

App Intents フレームワークには、ValueRepresentation、RelevantEntities、EntityCollection、SyncableEntity、@UnionValue、LongRunningIntent、CancellableIntent、ExecutionTargets などの機能が追加され、アプリ間のデータ共有はより滑らかに、大量エンティティの処理はより効率的に、デバイスをまたぐ会話はより確実に、長時間実行タスクはより制御しやすくなります。

主要内容

現実の問題から始める

旅行記録アプリがあり、ユーザーが保存した「ランドマーク」を共有したいとします。

ユーザーは Shortcuts で自動化を作りました。近くのランドマークを探し、メッセージと一緒に友人へ送ります。あなたのランドマークエンティティはすでに Transferable を実装しているため、Shortcuts はそれを Mail が使える形式へ変換できます。ここまでは問題ありません。

しかし、別の場面ではランドマークをマップアプリへ送り、ナビゲーションしたいとします。ここで問題が起きます。マップが必要とするのは座標や住所のような構造化データであり、ファイルではありません。既存の FileRepresentationDataRepresentation は PDF や画像のように明確なファイル形式を持つ型を扱えますが、座標のような純粋な構造化データには向きません。

これを解決するのが ValueRepresentation です。エンティティがシステム既知の構造化型を持てるようになり、アプリ間を流れることができます。

新しい「関連性」の仕組み

別の場面を考えます。音楽アプリを作り、ランニングに合う高 BPM のプレイリストを丁寧に選びました。

ユーザーがフィットネスアプリでランニングワークアウトを設定するとき、システムはいくつかのプレイリストを推薦します。あなたの新しいプレイリストは、どうすればこの一覧に入るでしょうか。

従来の方法は 2 つあります。Spotlight で内容をインデックスして検索可能にする方法と、インタラクション寄付でユーザーがアプリをどう使うかをシステムに伝える方法です。しかし新しいプレイリストには問題があります。ユーザーはその存在を知らず、検索もしておらず、再生もしていないため、寄付できるインタラクションがありません。

必要なのは「このプレイリストはランニングの文脈で関連している」とシステムに伝える能力です。それが RelevantEntities です。

性能ボトルネックの解決

アプリのデータが増えると、ユーザーは数千枚の写真に一度にタグを付けたくなるかもしれません。

intent の定義は単純です。写真エンティティの配列を受け取り、各写真にキーワードを追加します。しかし実際の実行は遅くなります。システムは intent を実行する前にすべてのエンティティを解決し、各エンティティの全プロパティを取得するからです。あなたに必要なのは ID だけなのに、システムは写真のメタデータ全体を解決してしまいます。

EntityCollection はこの問題を修正します。完全なエンティティを解決する代わりに、エンティティ ID だけを渡します。

デバイスをまたぐエンティティ参照

ユーザーが iPhone 上の Siri に写真をアルバムへ追加させ、その後別のデバイスで同じ写真にタグを付けるよう Siri に頼みます。Siri はその写真を見つけられません。

理由は、各デバイスが生成するローカル ID が違うからです。デバイスをまたいで会話を続けるには、Siri にすべてのデバイスで同じ安定 ID が必要です。サーバー UUID や CloudKit record ID のようなものです。

SyncableEntity は、このエンティティの ID がデバイス間で安定していることをシステムへ伝えるためのものです。

その他の強化

パラメータ型は DurationPersonNameComponents などのネイティブ型をサポートするようになりました。1 つのパラメータが複数の型を受け取ることも @UnionValue で可能です。intent は LongRunningIntent により 30 秒を超えて実行でき、CancellableIntent でキャンセルを適切に処理でき、ExecutionTargets で実行プロセスを指定できます。

詳細

ValueRepresentation: 構造化データを共有する

([0:42](https://developer.apple.com/videos/play/wwdc2026/345/?time=42))

ValueRepresentation により、エンティティは GeoToolbox フレームワーク由来の PlaceDescriptor のような、システムが理解している構造化型を持てます。

struct LandmarkEntity: AppEntity, Transferable {
    var id: Int
    var landmark: Landmark  // CLLocationCoordinate2D を含む

    static var transferRepresentation: some TransferRepresentation {
        ValueRepresentation(
            exporting: { entity in
                PlaceDescriptor(
                    representations: [.coordinate(entity.landmark.locationCoordinate)],
                    commonName: entity.landmark.name
                )
            }
        )
    }
}

要点:

  • ValueRepresentationTransferRepresentation の新しい種類です
  • exporting クロージャは、ここでは PlaceDescriptor というシステム既知の型を返します
  • PlaceDescriptor は座標と名前を含むため、マップアプリは直接ナビゲーションできます

エンティティがすでに PlaceDescriptor プロパティを持っている場合は、より簡潔な key-path 形式を使えます。

struct LandmarkEntity: AppEntity, Transferable {
    var id: Int
    @Property var placeDescriptor: PlaceDescriptor

    static var transferRepresentation: some TransferRepresentation {
        ValueRepresentation(exporting: \.placeDescriptor)
    }
}

RelevantEntities: 関連エンティティを登録する

([5:06](https://developer.apple.com/videos/play/wwdc2026/345/?time=306))

RelevantEntities API により、どのエンティティがどの文脈で関連するかをシステムへ伝えられます。

// ランニングワークアウト向けにプレイリストを推薦する
let playlistEntities = [dailyRun, runningMix]
let workoutContext = AppEntityContext.audio(.workout(activityType: .running))

try await RelevantEntities.shared.updateEntities(
    playlistEntities, for: workoutContext
)

// 特定コンテキストのすべてのエンティティを消去する
try await RelevantEntities.shared.removeAllEntities(for: workoutContext)

// 特定コンテキストから特定エンティティを削除する
try await RelevantEntities.shared.removeEntities(playlistEntities, from: workoutContext)

// すべてのコンテキストのすべてのエンティティを削除する
try await RelevantEntities.shared.removeAllEntities()

要点:

  • AppEntityContext.audio(.workout(activityType:)) は、音声に関係するフィットネス文脈を定義します
  • updateEntities(_:for:) はエンティティを登録し、システムが適切な場面で推薦できるようにします
  • エンティティは、明示的に削除するまで登録されたままです
  • removeAllEntities() はコンテキスト単位でもグローバルでも消去できます

3 つの内容発見方法の選び方:

  • Spotlight インデックス: 内容を検索可能にし、Siri から取得できるようにする
  • インタラクション寄付: ユーザーがアプリをどう使うかを Siri に教え、パターンを認識して反復操作を提案できるようにする
  • RelevantEntities: 特定の文脈でどの内容が関連するかをシステムへ伝える

EntityCollection: 大きなエンティティ集合を効率的に処理する

([7:12](https://developer.apple.com/videos/play/wwdc2026/345/?time=432))

パラメータ型を配列から EntityCollection に変えると、システムは完全なエンティティではなく ID だけを渡します。

struct TagPhotosIntent: AppIntent {
    static let title: LocalizedStringResource = "旅行写真にタグを付ける"

    @Parameter var photos: EntityCollection<PhotoEntity>   // 以前は: [PhotoEntity]
    @Parameter var tag: String

    func perform() async throws -> some IntentResult {
        modelData.tagPhotos(ids: photos.identifiers, tag: tag)   // 以前は: tagPhotos(photos, tag: tag)
        return .result()
    }
}

要点:

  • EntityCollection<PhotoEntity>[PhotoEntity] を置き換えます
  • photos.identifiers には完全なエンティティデータではなく ID だけが含まれます
  • 数百、数千のエンティティを解決する性能コストを避けられます
  • データモデル側のメソッドは、レコード更新に ID だけを使えば済みます

SyncableEntity: デバイス間で安定した ID

([9:49](https://developer.apple.com/videos/play/wwdc2026/345/?time=589))

サーバー UUID や CloudKit record ID のように、ID がすでにデバイス間で安定している場合:

struct PhotoEntity: AppEntity, SyncableEntity {
    var id: String  // すでにデバイス間で安定している。これだけで十分
}

ローカル ID を使う場合は、安定 ID と組み合わせます。

struct PhotoEntity: AppEntity, SyncableEntity {
    var id: SyncableEntityIdentifier<String, String>

    init(localID: String, stableID: String) {
        self.id = SyncableEntityIdentifier(local: localID, stable: stableID)
    }
}

要点:

  • SyncableEntity プロトコルは、ID がデバイス間で安定していることを宣言します
  • ID そのものが安定している場合は、プロトコルを追加するだけで十分です
  • SyncableEntityIdentifier<Local, Stable> はローカル ID と安定 ID を対応付けます
  • デバイス上のローカルコードはローカル ID を使い、デバイスをまたぐシステム処理は安定 ID を使います

@UnionValue: 複数の型を受け取る

([11:58](https://developer.apple.com/videos/play/wwdc2026/345/?time=718))

1 つのパラメータが複数のエンティティ型を受け取れるようにします。

@UnionValue
enum TravelGalleryContent {
    case landmarkCollection(LandmarkCollectionEntity)
    case photoAlbum(PhotoAlbumEntity)

    static let typeDisplayRepresentation: TypeDisplayRepresentation = "旅行ギャラリー"
    static let caseDisplayRepresentations: [Cases: DisplayRepresentation] = [
        .landmarkCollection: "ランドマークコレクション",
        .photoAlbum: "写真アルバム"
    ]
}

要点:

  • @UnionValue マクロは、必要な型情報と選択 UI のサポートを生成します
  • 各 case は異なるエンティティ型を包みます
  • typeDisplayRepresentation は型全体のラベルです
  • caseDisplayRepresentations は各選択肢がピッカー内でどう表示されるかを定義します
  • Shortcuts アプリ内でも複数の選択肢として表示されます

LongRunningIntent + CancellableIntent: 長時間実行タスク

([13:41](https://developer.apple.com/videos/play/wwdc2026/345/?time=821))

intent を 30 秒以上実行し、キャンセルにも対応させます。

struct UploadPhotoIntent: LongRunningIntent, CancellableIntent {
    static let title: LocalizedStringResource = "写真をアップロード"

    @Parameter var photo: IntentFile

    func perform() async throws -> some IntentResult & ProvidesDialog {
        let result = try await performBackgroundTask {
            let chunks = calculateChunks(for: photo)
            progress.totalUnitCount = Int64(chunks)

            for chunk in 1...chunks {
                try Task.checkCancellation()
                try await uploadChunk(chunk)
                progress.completedUnitCount = Int64(chunk)
            }
            return "アップロードが完了しました!"
        } onCancel: { reason in
            cleanup(for: reason)
        }
        return .result(dialog: "\(result)")
    }
}

要点:

  • LongRunningIntent は 30 秒制限を超えた実行を許可します
  • performBackgroundTask は長時間かかる処理を包みます
  • progress オブジェクトは ProgressReportingIntent 由来で、システムが進捗表示に使います
  • 進捗は Live Activity として自動表示されます
  • Task.checkCancellation() はキャンセルされているかを確認します
  • CancellableIntentonCancel でキャンセル時のクリーンアップを処理します
  • ユーザーは Live Activity 上の停止ボタンを押せます

ExecutionTargets: 実行プロセスを制御する

([16:54](https://developer.apple.com/videos/play/wwdc2026/345/?time=1014))

intent をどのプロセスで実行するかを指定します。

// 書き込み操作: メインアプリが必要
struct UpdateFavoriteIntent: AppIntent {
    static var allowedExecutionTargets: ExecutionTargets { .main }
}

// 独立したダウンロード: 拡張で実行する
struct DownloadPhotoIntent: AppIntent {
    static var allowedExecutionTargets: ExecutionTargets { .appIntentsExtension }
}

// 読み取り専用の表示: widget 拡張で実行する
struct GetLandmarkStatusIntent: AppIntent {
    static var allowedExecutionTargets: ExecutionTargets { .widgetKitExtension }
}

// どちらでもよい: システムに選ばせる
struct TagPhotosIntent: AppIntent {
    static var allowedExecutionTargets: ExecutionTargets { [.main, .appIntentsExtension] }
}

要点:

  • 静的プロパティ allowedExecutionTargets はシステムのヒューリスティック選択を上書きします
  • .main はメインアプリでの実行を指定します
  • .appIntentsExtension は App Intents 拡張での実行を指定します
  • .widgetKitExtension は Widget 拡張での実行を指定します
  • 複数の選択肢を指定し、システムに選ばせることもできます

重要な示唆

1. ランドマーク共有アプリ

  • 何をするか: アプリ内のランドマークをマップアプリへ直接送り、ナビゲーションに使えるようにします。あるいはカレンダーアプリへイベントの場所として送ります。
  • なぜ価値があるか: ValueRepresentation により、エンティティは PlaceDescriptorEventDescriptor のようなシステム既知の型を持てるため、マップやカレンダーなどのシステムアプリと滑らかに連携できます。
  • どう始めるか: エンティティに ValueRepresentation を追加し、対応する Descriptor 型を export します。

2. 文脈認識型の音楽アプリ

  • 何をするか: ランニング、読書、睡眠など、ユーザーの現在の活動に合わせて適切なプレイリストを自動推薦します。
  • なぜ価値があるか: RelevantEntities により、「このプレイリストはランニング中に関連する」とシステムへ伝えられ、フィットネスアプリのワークアウト設定画面で推薦されます。
  • どう始めるか: AppEntityContext で文脈を定義し、RelevantEntities.shared.updateEntities(_:for:) でエンティティを登録します。

3. 写真の一括管理アプリ

  • 何をするか: ユーザーが数百、数千枚の写真へ一度にタグを付けたり、アルバムへ追加したりしても、もたつかないようにします。
  • なぜ価値があるか: EntityCollection は ID だけを渡し、大量エンティティを解決する性能コストを避けるため、一括操作がすぐに完了します。
  • どう始めるか: 配列を受け取る @Parameter を EntityCollection<YourEntityType> に変更し、identifiers プロパティで ID 配列を取得します。

4. クラウド同期ノートアプリ

  • 何をするか: ユーザーが iPhone 上でノートに「これを ToDo リストに入れて」と言い、その後 Mac 上で同じノートに「完了にして」と言えるようにします。
  • なぜ価値があるか: SyncableEntity により、Siri はデバイスをまたいで同じエンティティを参照でき、本当の意味でのクロスデバイス会話を実現できます。
  • どう始めるか: エンティティにサーバー UUID や CloudKit record ID のようなデバイス間で安定した ID があることを確認し、SyncableEntity プロトコルを追加します。

5. 大容量ファイルアップロードアプリ

  • 何をするか: Widget から大容量ファイルアップロードを開始し、リアルタイム進捗を表示し、いつでもキャンセルできるようにします。
  • なぜ価値があるか: LongRunningIntent によりタスクは 30 秒を超えて実行でき、進捗は Live Activity として自動表示され、CancellableIntent によりキャンセル時のクリーンアップも適切に行えます。
  • どう始めるか: LongRunningIntent と CancellableIntent を実装し、performBackgroundTask で処理を包み、定期的に progress を更新します。

関連セッション

コメント

GitHub Issues · utterances