2025 AI & Machine Learning
WWDC25 · 27 min · AI & Machine Learning
Explore new advances in App Intents
Explore all the new enhancements available in the App Intents framework in this year’s releases. Learn about developer quality-of-life improvements like deferred properties, new capabilities like interactive app intents snippets, entity view annotations, how to integrate Visual Intelligence, and much more. We’ll take you through how App Intents is more expressive than ever, while becoming even easier and smoother to adopt. We’ll also share exciting new clients of App Intents this year like Spotlight and Visual Intelligence, and learn to write app intents that work great in those contexts.
Watch at developer.apple.com ↗Chapters
Code shown on screen · 23 snippets
Returning a Snippet Intent
import AppIntents
import SwiftUI
struct ClosestLandmarkIntent: AppIntent {
static let title: LocalizedStringResource = "Find Closest Landmark"
var modelData: ModelData
func perform() async throws -> some ReturnsValue<LandmarkEntity> & ShowsSnippetIntent & ProvidesDialog {
let landmark = await self.findClosestLandmark()
return .result(
value: landmark,
dialog: IntentDialog(
full: "The closest landmark is \(landmark.name).",
supporting: "\(landmark.name) is located in \(landmark.continent)."
),
snippetIntent: LandmarkSnippetIntent(landmark: landmark)
)
}
} Building a SnippetIntent
struct LandmarkSnippetIntent: SnippetIntent {
static let title: LocalizedStringResource = "Landmark Snippet"
var landmark: LandmarkEntity
var modelData: ModelData
func perform() async throws -> some IntentResult & ShowsSnippetView {
let isFavorite = await modelData.isFavorite(landmark)
return .result(
view: LandmarkView(landmark: landmark, isFavorite: isFavorite)
)
}
} Associate intents with buttons
struct LandmarkView: View {
let landmark: LandmarkEntity
let isFavorite: Bool
var body: some View {
// ...
Button(intent: UpdateFavoritesIntent(landmark: landmark, isFavorite: !isFavorite)) { /* ... */ }
Button(intent: FindTicketsIntent(landmark: landmark)) { /* ... */ }
// ...
}
} Request confirmation snippet
struct FindTicketsIntent: AppIntent {
func perform() async throws -> some IntentResult & ShowsSnippetIntent {
let searchRequest = await searchEngine.createRequest(landmarkEntity: landmark)
// Present a snippet that allows people to change
// the number of tickets.
try await requestConfirmation(
actionName: .search,
snippetIntent: TicketRequestSnippetIntent(searchRequest: searchRequest)
)
// Resume searching...
}
} Using Entities as parameters
struct TicketRequestSnippetIntent: SnippetIntent {
static let title: LocalizedStringResource = "Ticket Request Snippet"
var searchRequest: SearchRequestEntity
func perform() async throws -> some IntentResult & ShowsSnippetView {
let view = TicketRequestView(searchRequest: searchRequest)
return .result(view: view)
}
} Updating a snippet
func performRequest(request: SearchRequestEntity) async throws {
// Set to pending status...
TicketResultSnippetIntent.reload()
// Kick off search...
TicketResultSnippetIntent.reload()
} Responding to Image Search
struct LandmarkIntentValueQuery: IntentValueQuery {
var modelData: ModelData
func values(for input: SemanticContentDescriptor) async throws -> [LandmarkEntity] {
guard let pixelBuffer: CVReadOnlyPixelBuffer = input.pixelBuffer else {
return []
}
let landmarks = try await modelData.searchLandmarks(matching: pixelBuffer)
return landmarks
}
} Support opening an entity
struct OpenLandmarkIntent: OpenIntent {
static var title: LocalizedStringResource = "Open Landmark"
(title: "Landmark")
var target: LandmarkEntity
func perform() async throws -> some IntentResult {
/// ...
}
} Show search results in app
(schema: .visualIntelligence.semanticContentSearch)
struct ShowSearchResultsIntent {
var semanticContent: SemanticContentDescriptor
var navigator: Navigator
func perform() async throws -> some IntentResult {
await navigator.showImageSearch(semanticContent.pixelBuffer)
return .result()
}
// ...
} Returning multiple entity types
enum VisualSearchResult {
case landmark(LandmarkEntity)
case collection(CollectionEntity)
}a
struct LandmarkIntentValueQuery: IntentValueQuery {
func values(for input: SemanticContentDescriptor) async throws -> [VisualSearchResult] {
// ...
}
}
struct OpenLandmarkIntent: OpenIntent { /* ... */ }
struct OpenCollectionIntent: OpenIntent { /* ... */ } Associating a view with an AppEntity
struct LandmarkDetailView: View {
let landmark: LandmarkEntity
var body: some View {
Group{ /* ... */ }
.userActivity("com.landmarks.ViewingLandmark") { activity in
activity.title = "Viewing \(landmark.name)"
activity.appEntityIdentifier = EntityIdentifier(for: landmark)
}
}
} Converting AppEntity to PDF
import CoreTransferable
import PDFKit
extension LandmarkEntity: Transferable {
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(exportedContentType: .pdf) {landmark in
// Create PDF data...
return data
}
}
} Associating properties with Spotlight keys
struct LandmarkEntity: IndexedEntity {
// ...
(indexingKey: \.displayName)
var name: String
(customIndexingKey: /* ... */)
var continent: String
// ...
} Making intents undoable
struct DeleteCollectionIntent: UndoableIntent {
// ...
func perform() async throws -> some IntentResult {
// Confirm deletion...
await undoManager?.registerUndo(withTarget: modelData) {modelData in
// Restore collection...
}
await undoManager?.setActionName("Delete \(collection.name)")
// Delete collection...
}
} Multiple choice
struct DeleteCollectionIntent: UndoableIntent {
func perform() async throws -> some IntentResult & ReturnsValue<CollectionEntity?> {
let archive = Option(title: "Archive", style: .default)
let delete = Option(title: "Delete", style: .destructive)
let resultChoice = try await requestChoice(
between: [.cancel, archive, delete],
dialog: "Do you want to archive or delete \(collection.name)?",
view: collectionSnippetView(collection)
)
switch resultChoice {
case archive: // Archive collection...
case delete: // Delete collection...
default: // Do nothing...
}
}
// ...
} Supported modes
struct GetCrowdStatusIntent: AppIntent {
static let supportedModes: IntentModes = [.background, .foreground]
func perform() async throws -> some ReturnsValue<Int> & ProvidesDialog {
if systemContext.currentMode == .foreground {
await navigator.navigateToCrowdStatus(landmark)
}
// Retrieve status and return dialog...
}
} Supported modes
struct GetCrowdStatusIntent: AppIntent {
static let supportedModes: IntentModes = [.background, .foreground(.dynamic)]
func perform() async throws -> some ReturnsValue<Int> & ProvidesDialog {
guard await modelData.isOpen(landmark) else { /* Exit early... */ }
if systemContext.currentMode.canContinueInForeground {
do {
try await continueInForeground(alwaysConfirm: false)
await navigator.navigateToCrowdStatus(landmark)
} catch {
// Open app denied.
}
}
// Retrieve status and return dialog...
}
} View Control
extension OpenLandmarkIntent: TargetContentProvidingIntent {}
struct LandmarksNavigationStack: View {
var path: [Landmark] = []
var body: some View {
NavigationStack(path: $path) { /* ... */ }
.onAppIntentExecution(OpenLandmarkIntent.self) { intent in
self.path.append(intent.landmark)
}
}
} Scene activation condition
@main
struct AppIntentsTravelTrackerApp: App {
var body: some Scene {
WindowGroup { /* ... */ }
WindowGroup { /* ... */ }
.handlesExternalEvents(matching: [
OpenLandmarkIntent.persistentIdentifier
])
}
} View activation condition
struct LandmarksNavigationStack: View {
var body: some View {
NavigationStack(path: $path) { /* ... */ }
.handlesExternalEvents(
preferring: [],
allowing: !isEditing ? [OpenLandmarkIntent.persistentIdentifier] : []
)
}
} Computed property
struct SettingsEntity: UniqueAppEntity {
var defaultPlace: PlaceDescriptor {
UserDefaults.standard.defaultPlace
}
init() {
}
} Deferred property
struct LandmarkEntity: IndexedEntity {
// ...
var crowdStatus: Int {
get async throws {
await modelData.getCrowdStatus(self)
}
}
// ...
} AppIntentsPackage
// Framework or dynamic library
public struct LandmarksKitPackage: AppIntentsPackage { }
// App target
struct LandmarksPackage: AppIntentsPackage {
static var includedPackages: [any AppIntentsPackage.Type] {
[LandmarksKitPackage.self]
}
} Resources
- Adopting App Intents to support system experiences
- Building a workout app for iPhone and iPad
- Accelerating app interactions with App Intents
- App schema domains
- Creating your first app intent
- Integrating your app with Siri and Apple Intelligence
- Making actions and content discoverable and widely available
- PurchaseIntent
- App Shortcuts
- App Intents
Related sessions
-
26 min -
9 min -
18 min -
22 min -
25 min -
19 min -
7 min