WWDC Quick Look 💓 By SwiftGGTeam
What's new in App Intents

What's new in App Intents

Watch original video

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 IndexedEntity protocol automatically converts DisplayRepresentation to CSSearchableItem attribute sets
  • The indexAppEntities method accepts an array of AppEntity and 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 attributeSet computed property lets you customize indexed metadata
  • You can add location info (city, state) to improve local search relevance
  • The keywords field 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:

  • transferRepresentation is a static computed property returning supported representations
  • Order matters: arrange from highest to lowest fidelity
  • DataRepresentation is for in-memory data (JSON, RTF, text)
  • FileRepresentation is for file types (PNG, PDF)
  • SavedFileRepresentable wraps 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:

  • IntentFile parameters can accept any Entity conforming to Transferable
  • The contentType property checks the actual content type
  • textContent and fileURL are 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:

  • FileEntity is a special AppEntity representing the file itself
  • supportedContentTypes declares supported file types
  • FileEntityIdentifier uses 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:

  • URLRepresentation uses string interpolation and can insert properties marked with @Property
  • URLRepresentableIntent combined with OpenIntent automatically handles URL opening
  • openUrlWhenRun controls 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 UnionValue macro 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

  1. 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 IndexedEntity conformance, and call indexAppEntities at app launch. If you already have CSSearchableItem code, use associateAppEntity to connect both.

  2. 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 Transferable support 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.

  3. 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 URLRepresentableEntity to Entities that need navigation, and URLRepresentableIntent to open-type Intents. Ensure your URL template includes the Entity ID or other unique identifier.

  4. 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.


Comments

GitHub Issues · utterances