2026 App ServicesAI & Machine Learning
WWDC26 · 24 min · App Services / AI & Machine Learning
Explore advanced App Intents features for Siri and Apple Intelligence
Polish how your app works with Siri using advanced App Intents APIs. Learn techniques that let people accomplish more with just their voice, help Apple Intelligence find your content, and provide context for on-screen awareness so Siri understands what’s happening in your app.
Watch at developer.apple.com ↗Chapters
- 0:00 — Introduction
- 1:59 — Customize how Siri responds
- 4:20 — Visual responses
- 6:22 — Interaction donations
- 9:46 — Confirmations and entity ownership
- 11:59 — Semantic index with IndexedEntity
- 13:32 — Structured search with IntentValueQuery
- 15:27 — In-app search
- 16:22 — Onscreen awareness
- 20:51 — Leverage existing integrations
- 23:30 — Next steps
Code shown on screen · 12 snippets
Custom dialog response
(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"
)
)
}
} Ask a clarifying question within an inten
(schema: .clock.createTimer)
struct CreateTimerIntent {
// MARK: Schema Parameters
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)
}
} Enhanced DisplayRepresentation
// Enhanced DisplayRepresentation
(schema: .audio.song)
struct SongEntity {
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: "\(artistName)",
image: artworkImage
)
}
} Return a custom snippet view
(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)
}
} Donate a UI interaction
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)
)
}
}
}
} Declare entity ownership for confirmations
// Informs system if entity is public or shared with others
(schema: .calendar.event)
struct EventEntity: OwnershipProvidingEntity {
var ownership: EntityOwnership {
// isShared used to compute ownership state: .shared, .public, or .unknown
attendees.isEmpty ? .unknown : .shared
}
} Index entities with IndexedEntity
// Indexing IndexedEntity with CSSearchableIndex
struct EntityIndexingHelper {
// Indexes playlist entities
func indexPlaylist(_ playlist: Playlist) async throws {
let entity = PlaylistEntity(playlist: playlist)
try await CSSearchableIndex(name: indexName)
.indexAppEntities([entity])
}
} Structured search with IntentValueQuer
// Structured search of songs and playlists
struct AudioIntentValueQuery: IntentValueQuery {
// AudioSearch, IntentPerson, and other system types may be supported as input
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
}
}
} Re-run Siri search in your app
// Intent that re-runs the Siri search in app
(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()
}
} Onscreen awareness annotations
// (a) Single primary entity on screen — NSUserActivity
struct NowPlayingView: View {
(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
)
}
}
}
// (b) One entity among many — 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)
)
}
}
// (c) Lists and collections — 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)
}
}
} Component-based display representation query
// Component-based display representation queries
extension PlaylistQuery {
func displayRepresentations(
for identifiers: [PlaylistEntity.ID],
requestedComponents: DisplayRepresentation.Components = .text
) async throws -> [PlaylistEntity.ID: DisplayRepresentation] {
let entities = try await model.playlistEntities(for: identifiers)
// Fetch display representations for fetched entities
var result: [PlaylistEntity.ID: DisplayRepresentation] = [:]
for entity in entities {
result[entity.id] = await entity.displayRepresentation(with: requestedComponents)
}
return result
}
} Entity annotations on system integrations
// (a) User notifications
import AppIntents
import UserNotifications
func scheduleNotification(message: Message, author: Contact, conversation: Conversation) {
let content = UNMutableNotificationContent()
content.title = author.name
content.body = message.body
// Annotate with entity identifier
content.appEntityIdentifiers = [
EntityIdentifier(for: MessageEntity.self, identifier: message.id)
]
// Schedule the notification
}
// (b) Now Playing — most specific to least specific
import NowPlaying
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
}
}
// (c) AlarmKit
import 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
)
// Schedule alarm
} Resources
Related sessions
-
27 min -
24 min