WWDC Quick Look 💓 By SwiftGGTeam
Explore advanced App Intents features for Siri and Apple Intelligence

Explore advanced App Intents features for Siri and Apple Intelligence

观看原视频

Highlight

App Intents 新增自定义对话响应、交互捐赠、实体所有权声明、屏幕感知和系统实体标注等 API,让应用在 Siri 和 Apple Intelligence 中提供更自然、更个性化的体验。

核心内容

以前让应用通过 Siri 执行操作,开发者能控制的很有限。Siri 会自动生成响应,开发者无法匹配应用的独特语气和视觉风格。当用户在应用内执行操作时,Apple Intelligence 无法学习这些行为模式。应用内容对 Siri 来说也不够透明,用户看到屏幕上的内容,Siri 可能”看不见”。

Apple 在 WWDC26 推出了 App Intents 高级功能,解决了这些限制。

开发者现在可以自定义 Siri 的对话响应,让回复匹配应用的语言风格。应用可以通过交互捐赠 API 告诉 Apple Intelligence 用户在 UI 中的操作,帮助系统理解用户偏好。实体所有权声明让 Siri 知道哪些内容是公开或共享的,从而决定是否需要确认。屏幕感知 API 将屏幕上显示的内容连接到 Siri 能理解的实体。系统级别的实体标注让 Siri 在通知、正在播放、闹钟等场景中也能理解应用内容。

这些功能让应用与 Siri 的协作更深入,用户体验更连贯。

详细内容

自定义对话响应 (02:00)

ProvidesDialog 协议让 intent 返回自定义对话。Siri 可以显示 supporting 文本,在语音设备上朗读 full 文本。

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

    func perform() async throws -> some IntentResult & ProvidesDialog {
        // Adds song to playlist and responds
        return .result(
            dialog: IntentDialog(
                full: """
                      Added \(song.title) to the \
                      \(playlist.title) mix tape.
                      """,
                supporting: "Added"
            )
        )
    }
}

关键点:

  • ProvidesDialog 协议声明 intent 提供自定义对话
  • IntentDialog 的 full 字段包含完整描述,供语音设备使用
  • 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> {
        // Checks active timers and requests label parameter
        label = try await $label.requestValue(
            """
            You already have a timer running. \
            What should we call this one?
            """
        )
        return .result(value: timerEntity)
    }
}

关键点:

  • $label.requestValue() 在执行过程中请求用户输入
  • 适用于需要澄清的场景,比如避免重名冲突

Intent 响应还可以返回自定义 SwiftUI 视图,使用 ShowsSnippetView 协议。

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

    var audioEntity: AudioEntity
    var playlist: PlaylistEntity

    func perform() async throws -> some IntentResult & ProvidesDialog & ShowsSnippetView {
        // Adds to playlist and shows dialog and snippet
        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 中的显示方式
  • 包含 title、subtitle、image 三个组件
  • 用于实体选择、确认对话等场景

交互捐赠 (07:00)

用户在应用 UI 中的操作,通过 IntentDonationManager 告诉 Apple Intelligence,帮助系统学习用户偏好。

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

        // Donate intent with parameters and result so Siri can learn user preferences
        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 used to compute ownership state: .shared, .public, or .unknown
        attendees.isEmpty ? .unknown : .shared
    }
}

关键点:

  • 公开或共享实体的操作更可能触发确认
  • 默认是 .unknown,系统假设为私有
  • 应根据实体状态动态返回

语义索引与结构化搜索 (11:30)

使用 IndexedEntityCSSearchableIndex.indexAppEntities() 将实体加入 Spotlight 语义索引。

struct EntityIndexingHelper {
    // Indexes playlist entities
    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()
        // ... also a .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 {
        // Perform in-app search with Siri search string
        navigation.searchText = criteria.term
        navigation.selectedTab = .library
        return .result()
    }
}

关键点:

  • 适用于开发者精心设计的应用内搜索体验
  • 即使不索引实体也能工作
  • 将搜索字符串传递给应用导航系统

屏幕感知 (16:27)

屏幕感知让 Siri 理解屏幕上显示的实体。主要有三种 API。

单一主实体使用 NSUserActivity

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

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

关键点:

  • 用于整个屏幕展示单一实体的场景
  • 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)

在通知、正在播放、闹钟等系统场景中标注实体。

// User notifications
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
    )
}

关键点:

  • 三个场景使用相同的标注模式
  • 按从具体到一般的顺序排列实体标识符
  • 不能使用 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 理解屏幕上显示的内容。

为什么值得做:用户看到屏幕上的内容后,可以直接说”播放第三个”或”分享那个”,而不必说出完整名称。这消除了”用户看到了但 Siri 看不到”的断层,让语音交互更自然。

怎么开始:单一实体用 .userActivityappEntityIdentifier,少量实体用 .appEntityIdentifier 修饰 View,大量列表用 .appEntityIdentifier(forSelectionType:) 做 Collection 标注。入口:appEntityIdentifier + EntityIdentifier(for:identifier:)

关联 Session

评论

GitHub Issues · utterances