WWDC Quick Look 💓 By SwiftGGTeam
What's new in App Intents

What's new in App Intents

元の動画を見る

ハイライト

App Intents は 2024 年に 4 つの主要な更新を導入しました。IndexedEntity でエンティティを Spotlight にインデックス、Transferable で標準フォーマットに変換、URLRepresentable で Intent タイプを Universal Link にマッピング、UnionValue などの開発者体験の改善です。


主要内容

以前、app のコンテンツは CSSearchableItem として Spotlight に表示されるだけでした。検索能力が制限され、ユーザーがコンテンツを見つけてもシステムは追加操作ができませんでした。Siri も app 内部のエンティティ理解が限定的で、キーワードマッチングに依存し、コンテンツの意味を真に理解できませんでした。

2024 年、Apple は IndexedEntity プロトコルを導入しました。App Entity を CSSearchableItem 経由なしで Spotlight に直接ドネートできます。さらに重要なのは、Siri と Apple Intelligence が app コンテンツを理解する道が開けたことです。Spotlight でトレイルを検索すると、Siri は文字列マッチだけでなく「Trail」エンティティとして理解し、属性と実行可能な操作を把握します。

もう一つの課題は app 間共有でした。メールや SNS へ app コンテンツを共有する際、クリップボードかカスタム共有拡張しかありませんでした。Entity を Transferable に準拠させると、PDF、画像、リッチテキストなど標準フォーマットに直接変換でき、それらを受け入れる任意の app で受け取れます。

Universal Links 統合はディープリンク問題を解決します。以前は Intent タイプを URL に直接マッピングできず、Shortcuts や Siri が app の特定ページへジャンプしにくい状態でした。URLRepresentableIntent により Intent タイプが Universal Link を自動生成し、既存の URL 処理コードを再利用できます。


詳細

IndexedEntity:エンティティを Spotlight にインデックス

IndexedEntity は App Entity を CSSearchableIndex で直接インデックスできる新プロトコルです(02:02)。最も基本的な実装はプロトコル準拠と app 起動時の indexAppEntities 呼び出しだけです:

// TrailEntity は IndexedEntity プロトコルに準拠
struct TrailEntity: IndexedEntity {
    // デフォルト実装は DisplayRepresentation で属性セットを埋める
}

// App の init メソッドですべてのエンティティをインデックス
class AppDelegate {
    init() {
        let trails = DataManager.shared.trails
        CSSearchableIndex.default().indexAppEntities(trails)
    }
}

キーポイント:

  • IndexedEntity プロトコルは DisplayRepresentation を CSSearchableItem の属性セットに自動変換
  • indexAppEntitiesAppEntity 配列を受け取り、複数エンティティを一括インデックス
  • インデックス後、エンティティは Spotlight 検索結果に表示され、Intent と関連付け可能

デフォルト実装は DisplayRepresentation のみ使用。attributeSet をカスタマイズして位置情報やキーワードなど追加データを提供できます(02:47):

extension TrailEntity {
    var attributeSet: CSSearchableItemAttributeSet {
        let attributeSet = CSSearchableItemAttributeSet(contentType: .UTTypeText)
        // 位置情報を追加
        attributeSet.city = location.city
        attributeSet.state = location.state
        // 検索を助けるキーワードを追加
        attributeSet.keywords = activities.map { $0.rawValue }
        return attributeSet
    }
}

キーポイント:

  • attributeSet 計算プロパティでインデックスメタデータをカスタマイズ
  • 位置情報(citystate)でローカル検索の関連性を向上
  • keywords フィールドで同義語や関連語を追加し、ユーザーがコンテンツを見つけやすく

既に CSSearchableItem でインデックスしている場合、新しい associateAppEntity メソッドで既存インデックスと App Entity を関連付け(04:12)、インデックスロジックの再実装は不要です。

Transferable:エンティティを app 間で共有

Transferable はモデルのシリアライズ/デシリアライズを記述する宣言的プロトコルです。App Entity も Transferable に準拠できます(05:05):

// Activity Summary Entity は Transferable プロトコルに準拠
struct ActivitySummaryEntity: AppEntity, Transferable {
    static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "アクティビティ概要")
    static var defaultQuery = ActivitySummaryQuery()

    var id: String
    var date: Date
    var caloriesBurned: Int
    var distance: Double

    // 転送可能な表現形式を定義
    static var transferRepresentation: some TransferRepresentation {
        // 1. 最高の忠実度:プライベートな Codable 表現
        DataRepresentation(contentType: .applicationJSON) { summary in
            try JSONEncoder().encode(summary)
        } importing: { data in
            try JSONDecoder().decode(ActivitySummaryEntity.self, from: data)
        }

        // 2. リッチテキスト形式
        DataRepresentation(contentType: .rtf) { summary in
            summary.toRichText()
        }

        // 3. 画像形式
        FileRepresentation(contentType: .png) { summary in
            let image = summary.generateChartImage()
            return SavedFileRepresentable(image)
        }

        // 4. プレーンテキスト形式(最低の忠実度)
        DataRepresentation(contentType: .plainText) { summary in
            summary.description.data(using: .utf8)!
        }
    }
}

キーポイント:

  • transferRepresentation はサポートする表現形式リストを返す静的計算プロパティ
  • 順序が重要:最高から最低の忠実度で並べる
  • DataRepresentation はメモリ内データ(JSON、RTF、テキスト)用
  • FileRepresentation はファイルタイプ(PNG、PDF)用
  • SavedFileRepresentable は生成した一時ファイルをラップし、他 app からアクセス可能に

Shortcuts では、Entity が異なるパラメータタイプを受け入れる Action に渡されると、App Intents が最適な表現形式を自動選択して変換します(07:02)。

Transferable と AppEntity の組み合わせには現時点で制限があります(08:00):Xcode はコンパイル時に Transferable 表現を理解する必要があり、ProxyRepresentation@Property 属性付きエンティティプロパティのみ参照可能です。

IntentFile:変換可能なコンテンツを受け取る

Intent が IntentFile パラメータを受け入れる場合、サポートするコンテンツタイプを宣言できます(08:49):

struct AppendToNoteIntent: AppIntent {
    static var description = "内容をメモに追加"

    @Parameter(title: "添付ファイル")
    var attachment: IntentFile

    @Parameter(title: "メモ")
    var note: NoteEntity

    static var parameterSummary = some ParameterSummary {
        Summary("\(\.$attachment)\(\.$note) に追加")
    }

    func perform() async throws -> some IntentResult & ReturnsValue<NoteEntity> {
        // ファイルタイプを確認
        if attachment.contentType == .plainText {
            let textContent = try await attachment.textContent
            note.append(text: textContent)
        } else if attachment.contentType == .png {
            let imageURL = try await attachment.fileURL
            note.append(imageFrom: imageURL)
        }

        return .result(value: note)
    }
}

キーポイント:

  • IntentFile パラメータは Transferable に準拠する任意の Entity を受け入れ可能
  • contentType プロパティで実際のコンテンツタイプを確認
  • textContentfileURL はコンテンツ抽出用の新 API
  • Siri と Shortcuts が Entity を IntentFile サポートタイプに自動変換

FileEntity:ファイルをエンティティとして

ドキュメントベース app やファイル管理 app では、FileEntity でファイル自体を Entity にできます(10:18):

struct PhotoEntity: AppEntity, FileEntity {
    static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "写真")
    static var defaultQuery = PhotoQuery()

    var id: FileEntityIdentifier
    var name: String

    // FileEntity が要求するプロパティ
    static var supportedContentTypes: [UTType] {
        [.image, .png, .jpeg]
    }

    // ファイル URL を識別子として使用
    init(url: URL) {
        self.id = FileEntityIdentifier(url: url)
        self.name = url.lastPathComponent
    }
}

キーポイント:

  • FileEntity はファイル自体を表す特殊な AppEntity
  • supportedContentTypes でサポートするファイルタイプを宣言
  • FileEntityIdentifier は URL ブックマークデータを使用し、ファイル移動・リネーム後も有効
  • 他 app は IntentFile 経由でこれらのファイルに安全にアクセス

URLRepresentable:ディープリンク

URLRepresentableEntityURLRepresentableIntent で Entity と Intent を Universal Link として表現できます(12:06):

// Entity がリンク表現をサポート
extension TrailEntity: URLRepresentableEntity {
    static var urlRepresentation: URLRepresentation {
        URLRepresentation(string: "https://trails.example.com/trail/\(Self.self.\.id)")
    }
}

// Intent がリンク表現をサポート
struct OpenTrailIntent: AppIntent, OpenIntent, URLRepresentableIntent {
    static var description = "トレイル詳細を開く"
    static var openUrlWhenRun: Bool = true

    @Parameter(title: "トレイル")
    var trail: TrailEntity

    static var parameterSummary = some ParameterSummary {
        Summary("\(\.$trail) を開く")
    }

    // perform の実装は不要。App Intents が URL 処理コードを呼び出す
}

キーポイント:

  • URLRepresentation は文字列補間を使用し、@Property 付きプロパティを挿入可能
  • URLRepresentableIntentOpenIntent の組み合わせで URL オープンを自動処理
  • openUrlWhenRun で実行時に URL を開くか制御
  • perform 実装不要 — フレームワークが既存 Universal Link 処理コードを再利用

別の方法として perform から OpenURLIntent を返すこともできます(14:24):

struct CreateTrailIntent: AppIntent {
    func perform() async throws -> some IntentResult & OpensIntent {
        // 新しいトレイルを作成
        let newTrail = try await createTrail()

        // OpenURLIntent を返して新しく作成したトレイルを開く
        return .result(opensIntent: OpenURLIntent(newTrail))
    }
}

UnionValue:和型パラメータ

新しい UnionValue マクロで、パラメータが複数タイプのいずれかを受け入れられます(14:38):

enum DayPassType: UnionValue {
    case park(ParkEntity)
    case trail(TrailEntity)
}

struct BuyDayPassIntent: AppIntent {
    static var description = "1日券を購入"

    @Parameter(title: "チケット種別")
    var passType: DayPassType

    func perform() async throws -> some IntentResult {
        switch passType {
        case .park(let park):
            try await purchaseParkPass(for: park)
        case .trail(let trail):
            try await purchaseTrailPass(for: trail)
        }

        return .result()
    }
}

キーポイント:

  • UnionValue マクロは enum を和値タイプとしてマーク
  • 各 case は関連値を 1 つだけ持つ必要がある
  • すべての関連値タイプは互いに異なる必要がある
  • 「または」パラメータと考える:ParkEntity または TrailEntity

開発者体験の改善

Xcode 16.0 では AppEntity プロパティや AppIntent パラメータの title が必須ではなくなりました(16:02):

// Xcode 16 より前:title の指定が必須
@Parameter(title: "トレイルコレクション")
var trailCollection: TrailCollection

// Xcode 16:title は省略可能
@Parameter
var trailCollection: TrailCollection  // title として "Trail Collection" を自動生成

Xcode はプロパティ名から title を自動生成します。カスタムタイトル(「TrailCollection」ではなく「Featured Collection」など)が必要な場合は手動指定可能です。

Framework サポートも改善されました(17:05):Framework 内で AppEntity を定義し、App や Extension から参照できます。以前はすべてのタイプが同一モジュール内である必要がありました。


重要ポイント

  1. コアコンテンツに IndexedEntity をすぐ追加

    なぜ価値があるか:Apple Intelligence が app コンテンツを理解する最も直接的な道です。数行のコードでエンティティを Spotlight と Siri 提案に表示でき、ユーザーは app を開かずにコンテンツを発見・操作できます。

    始め方:最もよく使う Entity(ドキュメント、プロジェクト、記事)から、IndexedEntity 準拠を追加し app 起動時に indexAppEntities を呼び出す。既存の CSSearchableItem コードがある場合は associateAppEntity で接続。

  2. 共有可能なコンテンツに Transferable を実装

    なぜ価値があるか:ユーザーは app コンテンツをメール、メモ、SNS へ共有することが多い。Transferable により標準フォーマット(PDF、画像、リッチテキスト)で任意の app が受け取れ、共有体験が大幅に向上します。

    始め方:共有されそうなコンテンツ(レポート、サマリー、チャート)を特定し、対応 Entity に Transferable サポートを追加。表現形式は忠実度順:私有フォーマット(同一 app 転送用)→ 汎用フォーマット(RTF、PNG、PDF)→ プレーンテキスト。

  3. URLRepresentable でディープリンクを簡素化

    なぜ価値があるか:Universal Links はディープリンクの標準。URLRepresentableIntent により Intent が正しい URL を自動生成し、既存ルーティングコードを再利用、ナビゲーションロジックの重複実装が不要です。

    始め方:ジャンプが必要な Entity に URLRepresentableEntity、オープン系 Intent に URLRepresentableIntent を追加。URL テンプレートに Entity ID など一意識別子を含める。

  4. UnionValue でパラメータタイプを簡素化

    なぜ価値があるか:異なるオブジェクトタイプで同じ操作(「日票購入」が公園票またはトレイル票)が必要な場合、UnionValue が型安全な「または」関係を提供し、2 つの別 Intent より明確です。

    始め方:複数の類似タイプパラメータを受け入れる Intent を探し、UnionValue を使う単一 Intent に統合。各 case の関連値タイプが互いに異なることを確認。


関連セッション

コメント

GitHub Issues · utterances