Highlight
App Intents introduced four key updates in 2024: IndexedEntity lets your entities be indexed in Spotlight, Transferable lets entities convert to standard formats, URLRepresentable maps Intent types to Universal Links, and developer experience improvements such as UnionValue.
Core Content
In the past, your app content could only appear in Spotlight as CSSearchableItem. That limited search capabilities—after users found content, the system could not take further action. Siri also had limited understanding of entities inside your app, relying on keyword matching rather than truly understanding content semantics.
In 2024, Apple introduced the IndexedEntity protocol. Your App Entity can now be donated directly to Spotlight without going through CSSearchableItem. More importantly, this opens a channel for Siri and Apple Intelligence to understand your app content. When you search for a trail in Spotlight, Siri does not just match text—it understands this is a “Trail” entity, knows its properties, and knows what actions can be performed.
Another pain point was cross-app sharing. When users wanted to share your app content to email or social media, the only options were the clipboard or custom share extensions. Now, by making Entity conform to Transferable, your content can convert directly to standard formats like PDF, images, and rich text, and be received by any app that accepts those formats.
Universal Links integration solves deep linking. In the past, Intent types could not map directly to URLs, making it hard for Shortcuts and Siri to jump to specific pages in your app. Now, URLRepresentableIntent lets Intent types automatically generate Universal Links, reusing your existing URL handling code.
Detailed Content
IndexedEntity: Index Entities in Spotlight
IndexedEntity is a new protocol that lets your App Entity be indexed directly by CSSearchableIndex (02:02). The most basic implementation only requires conforming to the protocol, then calling indexAppEntities at app launch:
// TrailEntity conforms to the IndexedEntity protocol
struct TrailEntity: IndexedEntity {
// The default implementation uses DisplayRepresentation to populate the attribute set
}
// Index all entities in the app initialization method
class AppDelegate {
init() {
let trails = DataManager.shared.trails
CSSearchableIndex.default().indexAppEntities(trails)
}
}
Key points:
- The
IndexedEntityprotocol automatically convertsDisplayRepresentationto CSSearchableItem attribute sets - The
indexAppEntitiesmethod accepts an array ofAppEntityand indexes multiple entities at once - After indexing, entities appear in Spotlight search results and can be associated with your Intents
The default implementation only uses DisplayRepresentation. You can further customize attributeSet to provide additional data such as location and keywords (02:47):
extension TrailEntity {
var attributeSet: CSSearchableItemAttributeSet {
let attributeSet = CSSearchableItemAttributeSet(contentType: .UTTypeText)
// Add location information
attributeSet.city = location.city
attributeSet.state = location.state
// Add keywords to help search
attributeSet.keywords = activities.map { $0.rawValue }
return attributeSet
}
}
Key points:
- The
attributeSetcomputed property lets you customize indexed metadata - You can add location info (
city,state) to improve local search relevance - The
keywordsfield adds synonyms or related terms to help users find content
If you already index content with CSSearchableItem, use the new associateAppEntity method to link existing indexes with App Entity (04:12) without reimplementing indexing logic.
Transferable: Share Entities Across Apps
Transferable is a declarative protocol describing how your model serializes and deserializes. App Entity can now also conform to Transferable (05:05):
// Activity Summary Entity conforms to the Transferable protocol
struct ActivitySummaryEntity: AppEntity, Transferable {
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Activity Summary")
static var defaultQuery = ActivitySummaryQuery()
var id: String
var date: Date
var caloriesBurned: Int
var distance: Double
// Define transferable representations
static var transferRepresentation: some TransferRepresentation {
// 1. Highest fidelity: private Codable representation
DataRepresentation(contentType: .applicationJSON) { summary in
try JSONEncoder().encode(summary)
} importing: { data in
try JSONDecoder().decode(ActivitySummaryEntity.self, from: data)
}
// 2. Rich text format
DataRepresentation(contentType: .rtf) { summary in
summary.toRichText()
}
// 3. Image format
FileRepresentation(contentType: .png) { summary in
let image = summary.generateChartImage()
return SavedFileRepresentable(image)
}
// 4. Plain text format, the lowest fidelity
DataRepresentation(contentType: .plainText) { summary in
summary.description.data(using: .utf8)!
}
}
}
Key points:
transferRepresentationis a static computed property returning supported representations- Order matters: arrange from highest to lowest fidelity
DataRepresentationis for in-memory data (JSON, RTF, text)FileRepresentationis for file types (PNG, PDF)SavedFileRepresentablewraps generated temporary files for other apps to access
In Shortcuts, when your Entity is passed to an Action accepting different parameter types, App Intents automatically selects the most appropriate representation for conversion (07:02).
Transferable combined with AppEntity currently has some limitations (08:00): Xcode must understand your Transferable representations at compile time, and ProxyRepresentation can only reference entity properties marked with @Property.
IntentFile: Receive Transferable Content
When your Intent accepts an IntentFile parameter, you can declare supported content types (08:49):
struct AppendToNoteIntent: AppIntent {
static var description = "Append content to a note"
@Parameter(title: "Attachment")
var attachment: IntentFile
@Parameter(title: "Note")
var note: NoteEntity
static var parameterSummary = some ParameterSummary {
Summary("Append \(\.$attachment) to \(\.$note)")
}
func perform() async throws -> some IntentResult & ReturnsValue<NoteEntity> {
// Check the file type
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)
}
}
Key points:
IntentFileparameters can accept any Entity conforming to Transferable- The
contentTypeproperty checks the actual content type textContentandfileURLare new APIs for extracting content- Siri and Shortcuts automatically convert Entity to types supported by IntentFile
FileEntity: Files as Entities
For document-based apps or apps managing files, use FileEntity to make files themselves entities (10:18):
struct PhotoEntity: AppEntity, FileEntity {
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Photo")
static var defaultQuery = PhotoQuery()
var id: FileEntityIdentifier
var name: String
// Properties required by FileEntity
static var supportedContentTypes: [UTType] {
[.image, .png, .jpeg]
}
// Use the file URL as the identifier
init(url: URL) {
self.id = FileEntityIdentifier(url: url)
self.name = url.lastPathComponent
}
}
Key points:
FileEntityis a specialAppEntityrepresenting the file itselfsupportedContentTypesdeclares supported file typesFileEntityIdentifieruses URL bookmark data, remaining valid after file moves or renames- Other apps can securely access these files through IntentFile
URLRepresentable: Deep Linking
URLRepresentableEntity and URLRepresentableIntent let your Entity and Intent be represented as Universal Links (12:06):
// Entity supports link representation
extension TrailEntity: URLRepresentableEntity {
static var urlRepresentation: URLRepresentation {
URLRepresentation(string: "https://trails.example.com/trail/\(Self.self.\.id)")
}
}
// Intent supports link representation
struct OpenTrailIntent: AppIntent, OpenIntent, URLRepresentableIntent {
static var description = "Open trail details"
static var openUrlWhenRun: Bool = true
@Parameter(title: "Trail")
var trail: TrailEntity
static var parameterSummary = some ParameterSummary {
Summary("Open \(\.$trail)")
}
// No need to implement perform; App Intents calls the URL handling code
}
Key points:
URLRepresentationuses string interpolation and can insert properties marked with@PropertyURLRepresentableIntentcombined withOpenIntentautomatically handles URL openingopenUrlWhenRuncontrols whether to open the URL when executed- No need to implement
perform—the framework reuses your existing Universal Link handling code
Another approach is returning OpenURLIntent from perform (14:24):
struct CreateTrailIntent: AppIntent {
func perform() async throws -> some IntentResult & OpensIntent {
// Create a new trail
let newTrail = try await createTrail()
// Return OpenURLIntent to open the newly created trail
return .result(opensIntent: OpenURLIntent(newTrail))
}
}
UnionValue: Union Type Parameters
The new UnionValue macro lets parameters accept one of multiple types (14:38):
enum DayPassType: UnionValue {
case park(ParkEntity)
case trail(TrailEntity)
}
struct BuyDayPassIntent: AppIntent {
static var description = "Buy a day pass"
@Parameter(title: "Pass Type")
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()
}
}
Key points:
- The
UnionValuemacro marks an enum as a union value type - Each case must have exactly one associated value
- All associated value types must be distinct
- Think of it as an “or” parameter: ParkEntity or TrailEntity
Developer Experience Improvements
Xcode 16.0 no longer requires titles for AppEntity properties or AppIntent parameters (16:02):
// Before Xcode 16: title was required
@Parameter(title: "Trail Collection")
var trailCollection: TrailCollection
// Xcode 16: title is optional
@Parameter
var trailCollection: TrailCollection // Automatically generates "Trail Collection" as the title
Xcode intelligently generates titles from property names. If you need custom titles (such as “Featured Collection” instead of “TrailCollection”), you can still specify them manually.
Framework support also improved (17:05): you can now define AppEntity in a Framework and reference it from an App or Extension. Previously all types had to be in the same module.
Core Takeaways
-
Add IndexedEntity to core content immediately
Why it’s worth doing: This is the most direct path for Apple Intelligence to understand your app content. A few lines of code let your entities appear in Spotlight and Siri suggestions, letting users discover and interact with your content without opening the app.
How to start: Begin with your most commonly used Entity (documents, projects, articles), add
IndexedEntityconformance, and callindexAppEntitiesat app launch. If you already haveCSSearchableItemcode, useassociateAppEntityto connect both. -
Implement Transferable for shareable content
Why it’s worth doing: Users often need to share your app content to email, notes, or social media. Transferable lets your content be received by any app in standard formats (PDF, images, rich text), greatly improving the sharing experience.
How to start: Identify content users might want to share (reports, summaries, charts) and add
Transferablesupport to the corresponding Entity. Order representations by fidelity: your private format first (for same-app transfer), then universal formats (RTF, PNG, PDF), finally plain text. -
Simplify deep linking with URLRepresentable
Why it’s worth doing: Universal Links are the standard for deep linking. With
URLRepresentableIntent, your Intent automatically generates the correct URL, reusing existing routing code without duplicating navigation logic.How to start: Add
URLRepresentableEntityto Entities that need navigation, andURLRepresentableIntentto open-type Intents. Ensure your URL template includes the Entity ID or other unique identifier. -
Simplify parameter types with UnionValue
Why it’s worth doing: When users need to perform the same action on different object types (such as “buy day pass” for park or trail passes), UnionValue provides a type-safe way to express “or” relationships, clearer than creating two separate Intents.
How to start: Find Intents accepting multiple similar type parameters and merge them into a single Intent using
UnionValue. Ensure each case’s associated value type is distinct.
Related Sessions
- Bring your app to Siri — Learn how to use SiriKit and App Intents to let users access your app features through voice
- Bring your app’s core features to users with App Intents — Deep dive into App Intents core concepts including Intent, Entity, and Query
- Control your app from anywhere with Controls — Learn how to build Control Center controls using App Intents
- Bring your Live Activity to Apple Watch — Bring Live Activities to the Smart Stack on Apple Watch
- What’s new in SwiftUI — SwiftUI’s annual update with deep App Intents integration
Comments
GitHub Issues · utterances