WWDC Quick Look 💓 By SwiftGGTeam
Siri と Apple Intelligence 向け App Intents の高度な機能を探る

Siri と Apple Intelligence 向け App Intents の高度な機能を探る

元の動画を見る

ハイライト

App Intents には、カスタム対話応答、インタラクションの寄付、エンティティ所有権の宣言、画面認識、システムエンティティ注釈などの API が追加され、アプリは Siri と Apple Intelligence の中でより自然でパーソナライズされた体験を提供できます。

主要内容

以前は、アプリの操作を Siri から実行できるようにしても、開発者が制御できる範囲は限られていました。Siri は応答を自動生成するため、アプリ独自の語り口や視覚スタイルには合わせられませんでした。ユーザーがアプリ内で操作しても、Apple Intelligence はその行動パターンを学習できませんでした。アプリの内容も Siri から十分に見えるわけではなく、ユーザーには画面上の内容が見えていても、Siri には「見えていない」ことがありました。

Apple は WWDC26 で、こうした制限を解決する App Intents の高度な機能を発表しました。

開発者は Siri の対話応答をカスタマイズし、アプリの言葉遣いに合った返答を返せます。アプリはインタラクション寄付 API を通じて、ユーザーが UI 上で行った操作を Apple Intelligence に伝え、システムがユーザーの好みを理解できるようにします。エンティティ所有権の宣言により、Siri はどの内容が公開または共有されているかを把握し、確認が必要かどうかを判断できます。画面認識 API は、画面上に表示されている内容を Siri が理解できるエンティティに結び付けます。システムレベルのエンティティ注釈により、通知、Now Playing、アラームなどの場面でも Siri がアプリの内容を理解できます。

これらの機能により、アプリと Siri の連携はより深くなり、ユーザー体験はより一貫したものになります。

詳細

カスタム対話応答 (02:00)

ProvidesDialog プロトコルを使うと、intent はカスタム対話を返せます。Siri は supporting テキストを表示し、音声デバイスでは full テキストを読み上げられます。

@AppIntent(schema: .audio.addToPlaylist)
struct AddToPlaylistIntent {

    func perform() async throws -> some IntentResult & ProvidesDialog {
        // 曲をプレイリストに追加して応答する
        return .result(
            dialog: IntentDialog(
                full: """
                      \(song.title)\
                      \(playlist.title) ミックステープに追加しました。
                      """,
                supporting: "追加しました"
            )
        )
    }
}

要点:

  • ProvidesDialog プロトコルは、intent がカスタム対話を提供することを宣言します
  • IntentDialogfull フィールドには、音声デバイス向けの完全な説明を入れます
  • supporting フィールドは UI に表示できる短い版です

intent の実行中には、$label.requestValue() を使ってユーザーに質問できます。

@AppIntent(schema: .clock.createTimer)
struct CreateTimerIntent {
    var duration: Duration
    var label: String?
    var isSleepTimer: Bool

    func perform() async throws -> some ReturnsValue<TimerEntity> {
        // 実行中のタイマーを確認し、label パラメータを要求する
        label = try await $label.requestValue(
            """
            すでにタイマーが実行中です。\
            このタイマーには何という名前を付けますか?
            """
        )
        return .result(value: timerEntity)
    }
}

要点:

  • $label.requestValue() は実行中にユーザー入力を要求します
  • 名前の重複を避けるなど、追加確認が必要な場面に適しています

intent の応答は、ShowsSnippetView プロトコルを使ってカスタム SwiftUI ビューも返せます。

@AppIntent(schema: .audio.addToPlaylist)
struct AddToPlaylistIntent {

    var audioEntity: AudioEntity
    var playlist: PlaylistEntity

    func perform() async throws -> some IntentResult & ProvidesDialog & ShowsSnippetView {
        // プレイリストに追加し、対話とスニペットを表示する
        let view = PlaylistSnippetView(
            playlist: updatedEntity,
            tracks: updated.tracks
        )
        return .result(dialog: dialog, view: view)
    }
}

エンティティの表示表現 (04:26)

エンティティは DisplayRepresentation を通じて、システム内での視覚的な表示を定義できます。

@AppEntity(schema: .audio.song)
struct SongEntity {

    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(
            title: "\(title)",
            subtitle: "\(artistName)",
            image: artworkImage
        )
    }
}

要点:

  • DisplayRepresentation は Siri、Spotlight、Shortcuts でのエンティティ表示を定義します
  • titlesubtitleimage の 3 つの要素を含みます
  • エンティティ選択、確認ダイアログなどの場面で使われます

インタラクション寄付 (07:00)

ユーザーがアプリ UI で行った操作を IntentDonationManager で Apple Intelligence に伝えると、システムはユーザーの好みを学習できます。

@ModelActor
actor ModelManager {
    func sendMessage(_ /* ... */, donateIntent: Bool = false) async throws -> [Message.ID] {

        // Siri がユーザーの好みを学習できるよう、パラメータと結果を含む intent を寄付する
        if donateIntent {
            let intent = SendMessageIntent()
            intent.destination = .recipients(conversation.recipients.map(\.entity))

            let result = messages.map(\.entity)
            Task {
                try await IntentDonationManager.shared.donate(
                    intent: intent,
                    result: .result(value: result)
                )
            }
        }
    }
}

要点:

  • 寄付が必要なのは UI インタラクション時だけで、Siri が呼び出した intent は自動的に記録されます
  • intent のパラメータと実行結果を渡す必要があります
  • システムはこの情報から、特定の連絡先にはどのアプリをよく使うか、といった好みを推定します

過剰な寄付はシステムに無視されるため、本当にユーザーの行動を表すときだけ寄付します。

エンティティ所有権の宣言 (10:03)

OwnershipProvidingEntity プロトコルは、エンティティが公開されたものか共有されたものかを Siri に伝え、確認方針に影響します。

@AppEntity(schema: .calendar.event)
struct EventEntity: OwnershipProvidingEntity {

    var ownership: EntityOwnership {
        // isShared を使って所有状態を計算する: .shared、.public、または .unknown
        attendees.isEmpty ? .unknown : .shared
    }
}

要点:

  • 公開または共有エンティティへの操作は、確認を求められやすくなります
  • デフォルトは .unknown で、システムは非公開と仮定します
  • エンティティの状態に応じて動的に返します

セマンティックインデックスと構造化検索 (11:30)

IndexedEntityCSSearchableIndex.indexAppEntities() を使い、エンティティを Spotlight のセマンティックインデックスに追加します。

struct EntityIndexingHelper {
    // プレイリストエンティティをインデックスする
    func indexPlaylist(_ playlist: Playlist) async throws {
        let entity = PlaylistEntity(playlist: playlist)
        try await CSSearchableIndex(name: indexName)
            .indexAppEntities([entity])
    }
}

要点:

  • インデックス後は、Siri から名前でエンティティを検索できます
  • Spotlight でもこれらのエンティティを検索できます
  • 一部のドメインはキーワード一致だけでなく、セマンティック検索にも対応します

事前にインデックスしない内容には、IntentValueQuery を使って構造化検索を提供します。

struct AudioIntentValueQuery: IntentValueQuery {

    func values(for input: AudioSearch) async throws -> [AudioEntity] {
        switch input.criteria {
        case .searchQuery(let query):
            return try await searchResults(for: query)
        case .unspecified:
            return try await likedSongResults()
        // ... .url の case もある
        }
    }
}

要点:

  • IntentValueQuery は構造化された検索入力を受け取ります
  • UnionValue を通じて複数のエンティティ型を返せます
  • .searchQuery.unspecified.url などの条件をサポートします

アプリ内検索 (14:49)

.system.searchInApp schema を使うと、Siri は結果を直接表示するのではなく、アプリ内で検索を実行できます。

@AppIntent(schema: .system.searchInApp)
struct SearchAudioLibraryIntent {

    var criteria: StringSearchCriteria

    func perform() async throws -> some IntentResult {
        // Siri の検索文字列を使ってアプリ内検索を実行する
        navigation.searchText = criteria.term
        navigation.selectedTab = .library
        return .result()
    }
}

要点:

  • 開発者が丁寧に設計したアプリ内検索体験に適しています
  • エンティティをインデックスしていなくても機能します
  • 検索文字列をアプリのナビゲーションシステムへ渡します

画面認識 (16:27)

画面認識により、Siri は画面上に表示されているエンティティを理解できます。主な API は 3 種類あります。

単一の主要エンティティには NSUserActivity を使います。

struct NowPlayingView: View {
    @Environment(PlaybackController.self) private var playback

    var body: some View {
        VStack {
            // プレーヤー UI
        }
        .userActivity("cosmotunes.nowPlaying", isActive: playback.currentTrack) { activity in
            activity.title = playback.currentTrack?.title
            activity.appEntityIdentifier = EntityIdentifier(
                for: SongEntity.self,
                identifier: playback.currentTrack.id
            )
        }
    }
}

要点:

  • 画面全体が 1 つのエンティティを表示する場面で使います
  • appEntityIdentifier が activity を具体的なエンティティに結び付けます

リスト内の複数エンティティには View Entity annotation を使います。

struct AlbumView: View {
    private var header: some View {
        VStack(alignment: .leading, spacing: 6) {
            // ...
        }
        .appEntityIdentifier(
            EntityIdentifier(for: AlbumEntity.self, identifier: session.id.uuidString)
        )
    }
}

要点:

  • エンティティを表す各 view に注釈を追加します
  • 画面上に少数のエンティティがある場面に適しています

大量のエンティティには Collection annotation を使います。

struct PlaylistDetailView: View {
    var body: some View {
        List {
            ForEach(playlist.tracks) { track in
                PlaylistTrackRow(track: track)
            }
        }
        .appEntityIdentifier(forSelectionType: GeneratedTrack.ID.self) { trackID in
            EntityIdentifier(for: SongEntity.self, identifier: trackID)
        }
    }
}

要点:

  • 各リスト項目へ個別に注釈を付ける必要を避けられます
  • システムは必要に応じてエンティティ識別子を取得します
  • 画面外へスクロールしたエンティティにも対応します

カスタムキャンバスビューでは custom canvas view annotation を使います。詳しくはサンプルコードを参照してください。

システムエンティティ注釈 (21:07)

通知、Now Playing、アラームなどのシステム場面でエンティティに注釈を付けます。

// ユーザー通知
func scheduleNotification(message: Message, author: Contact, conversation: Conversation) {
    let content = UNMutableNotificationContent()
    content.title = author.name
    content.body = message.body

    content.appEntityIdentifiers = [
        EntityIdentifier(for: MessageEntity.self, identifier: message.id)
    ]
}
// Now Playing
final class CosmoTunesMediaSession: MediaSessionRepresentable {
    var content: (any MediaContentRepresentable)? {
        var content = MusicContent(id: track.id.uuidString, songTitle: track.title /* ... */)
        content.appEntityIdentifiers = [
            EntityIdentifier(for: SongEntity.self, identifier: track.id),
            EntityIdentifier(for: ArtistEntity.self, identifier: track.session.artistName),
            EntityIdentifier(for: PlaylistEntity.self, identifier: currentPlaylist.id),
        ]
        return content
    }
}
// AlarmKit
func scheduleAlarm(_ alarm: Alarm) async throws {
    let configuration = AlarmManager.AlarmConfiguration<CosmoTunesAlarmMetadata>.alarm(
        schedule: schedule,
        attributes: attributes,
        appEntityIdentifier: EntityIdentifier(for: AlarmEntity.self, identifier: alarm.id),
        stopIntent: DismissAlarmIntent(),
        secondaryIntent: SnoozeAlarmIntent(),
        sound: sound
    )
}

要点:

  • 3 つの場面はいずれも同じ注釈パターンを使います
  • エンティティ識別子は具体的なものから一般的なものへ並べます
  • 永続的な識別子が必要なため、TransientAppEntity は使えません

重要な示唆

1. パーソナライズされた対話応答

何をするか: アプリの重要な Intent にカスタム対話を追加し、Siri の返答がアプリ自身の言葉に聞こえるようにします。

なぜ価値があるか: Siri の自動生成応答は均一で、アプリ固有の語り口には合いません。カスタム対話により、ユーザーは Siri がそのアプリを本当に「理解している」と感じられます。曲の追加やタイマー作成など、高頻度の操作から始めると費用対効果が高くなります。

どう始めるか: Intent の perform()some IntentResult & ProvidesDialog を返すようにし、IntentDialog(full:supporting:) で完全版と短縮版の返答を定義します。入口: ProvidesDialog プロトコル + IntentDialog

2. UI 寄付

何をするか: メッセージ送信、ナビゲーション開始、音楽再生など、アプリの中心的な操作フローに Intent 寄付を追加します。

なぜ価値があるか: Apple Intelligence がパーソナライズされた提案を行うには、ユーザーがアプリ内で実際に何をしているかを理解する必要があります。寄付は「このユーザーはこの相手にメッセージを送るとき、このアプリをよく使う」とシステムに伝え、Siri が次回優先して提案できるようにします。

どう始めるか: 操作完了後に IntentDonationManager.shared.donate(intent:result:) を呼び、完全なパラメータを含む intent と実行結果を渡します。本当に UI インタラクションがあった場合だけ寄付し、Siri 呼び出しは自動記録に任せます。入口: IntentDonationManager.shared.donate(intent:result:)

3. エンティティ所有権の認識

何をするか: アプリに公開コンテンツや共有コンテンツがある場合は、OwnershipProvidingEntity を実装します。

なぜ価値があるか: 公開または共有エンティティへの操作は Siri の確認を求められやすくなり、ユーザーが共有データを誤って変更することを防げます。所有権の状態を明確にすると、Siri はいつ追加確認すべきかを判断でき、アプリへの信頼も高まります。

どう始めるか: Entity を OwnershipProvidingEntity に準拠させ、ownership プロパティを実装して、エンティティの状態に応じて .shared.public.unknown を動的に返します。入口: OwnershipProvidingEntity プロトコル + var ownership: EntityOwnership

4. セマンティックインデックス

何をするか: アプリ内の比較的安定したコンテンツエンティティを Spotlight インデックスに追加します。

なぜ価値があるか: ユーザーはコンテンツがどのアプリにあるかを忘れても、名前は覚えていることがあります。Spotlight のセマンティックインデックスに追加すると、システム検索からアプリ内の具体的な内容へ直接移動でき、発見の負担が下がります。

どう始めるか: Entity を IndexedEntity に準拠させ、CSSearchableIndex.indexAppEntities() を呼んでインデックスへ追加します。事前にインデックスしない内容には、IntentValueQuery で構造化検索を提供します。入口: IndexedEntity + CSSearchableIndex.indexAppEntities()

5. 画面認識

何をするか: アプリの詳細画面や一覧画面にエンティティ注釈を追加し、Siri が画面上の内容を理解できるようにします。

なぜ価値があるか: ユーザーは画面を見たあと、「3 つ目を再生して」「あれを共有して」と言えるようになり、完全な名前を言う必要がありません。「ユーザーには見えているが Siri には見えていない」という断絶をなくし、音声操作をより自然にします。

どう始めるか: 単一エンティティには .userActivityappEntityIdentifier を使い、少数エンティティには .appEntityIdentifier で View に注釈を付け、大量リストには .appEntityIdentifier(forSelectionType:) で Collection 注釈を使います。入口: appEntityIdentifier + EntityIdentifier(for:identifier:)

関連セッション

コメント

GitHub Issues · utterances