axiom-core-spotlight-ref

Core Spotlight & NSUserActivity Reference

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "axiom-core-spotlight-ref" with this command: npx skills add fotescodev/ios-agent-skills/fotescodev-ios-agent-skills-axiom-core-spotlight-ref

Core Spotlight & NSUserActivity Reference

Overview

Comprehensive guide to Core Spotlight framework and NSUserActivity for making app content discoverable in Spotlight search, enabling Siri predictions, and supporting Handoff. Core Spotlight directly indexes app content while NSUserActivity captures user engagement for prediction.

Key distinction Core Spotlight = indexing all app content; NSUserActivity = marking current user activity for prediction/handoff.

When to Use This Skill

Use this skill when:

  • Indexing app content (documents, notes, orders, messages) for Spotlight

  • Using NSUserActivity for Handoff or Siri predictions

  • Choosing between CSSearchableItem, IndexedEntity, and NSUserActivity

  • Implementing activity continuation from Spotlight results

  • Batch indexing for performance

  • Deleting indexed content

  • Debugging Spotlight search not finding app content

  • Integrating NSUserActivity with App Intents (appEntityIdentifier)

Do NOT use this skill for:

  • App Shortcuts implementation (use app-shortcuts-ref)

  • App Intents basics (use app-intents-ref)

  • Overall discoverability strategy (use app-discoverability)

Related Skills

  • app-intents-ref — App Intents framework including IndexedEntity

  • app-discoverability — Strategic guide for making apps discoverable

  • app-shortcuts-ref — App Shortcuts for instant availability

When to Use Each API

Use Case Approach Example

User viewing specific screen NSUserActivity

User opened order details

Index all app content CSSearchableItem

All 500 orders searchable

App Intents entity search IndexedEntity

"Find orders where..."

Handoff between devices NSUserActivity

Continue editing note on Mac

Background content indexing CSSearchableItem batch Index documents on launch

Apple guidance Use NSUserActivity for user-initiated activities (screens currently visible), not as a general indexing mechanism. For comprehensive content indexing, use Core Spotlight's CSSearchableItem.

Core Spotlight (CSSearchableItem)

Creating Searchable Items

import CoreSpotlight import UniformTypeIdentifiers

func indexOrder(_ order: Order) { // 1. Create attribute set with metadata let attributes = CSSearchableItemAttributeSet(contentType: .item) attributes.title = order.coffeeName attributes.contentDescription = "Ordered on (order.date.formatted())" attributes.keywords = ["coffee", "order", order.coffeeName.lowercased()] attributes.thumbnailData = order.imageData

// Optional: Add location
attributes.latitude = order.location.coordinate.latitude
attributes.longitude = order.location.coordinate.longitude

// Optional: Add rating
attributes.rating = NSNumber(value: order.rating)

// 2. Create searchable item
let item = CSSearchableItem(
    uniqueIdentifier: order.id.uuidString,        // Stable ID
    domainIdentifier: "orders",                   // Grouping
    attributeSet: attributes
)

// Optional: Set expiration
item.expirationDate = Date().addingTimeInterval(60 * 60 * 24 * 365)  // 1 year

// 3. Index the item
CSSearchableIndex.default().indexSearchableItems([item]) { error in
    if let error = error {
        print("Indexing error: \(error.localizedDescription)")
    }
}

}

Key Properties

uniqueIdentifier

Purpose Stable, persistent ID unique to this item within your app.

uniqueIdentifier: order.id.uuidString

Requirements:

  • Must be stable (same item = same identifier)

  • Used for updates and deletion

  • Scoped to your app

domainIdentifier

Purpose Groups related items for bulk operations.

domainIdentifier: "orders"

Use cases:

  • Delete all items in a domain

  • Organize by type (orders, documents, messages)

  • Batch operations

Pattern:

// Index with domains item1.domainIdentifier = "orders" item2.domainIdentifier = "documents"

// Delete entire domain CSSearchableIndex.default().deleteSearchableItems( withDomainIdentifiers: ["orders"] ) { error in }

CSSearchableItemAttributeSet

Metadata describing the searchable content.

let attributes = CSSearchableItemAttributeSet(contentType: .item)

// Required attributes.title = "Order #1234" attributes.displayName = "Coffee Order"

// Highly recommended attributes.contentDescription = "Medium latte with oat milk" attributes.keywords = ["coffee", "latte", "order"] attributes.thumbnailData = imageData

// Optional but valuable attributes.contentCreationDate = Date() attributes.contentModificationDate = Date() attributes.rating = NSNumber(value: 5) attributes.comment = "My favorite order"

Common Attributes

Attribute Purpose Example

title

Primary title "Coffee Order #1234"

displayName

User-visible name "Morning Latte"

contentDescription

Description text "Medium latte with oat milk"

keywords

Search terms ["coffee", "latte"]

thumbnailData

Preview image JPEG/PNG data

contentCreationDate

When created Date()

contentModificationDate

Last modified Date()

rating

Star rating NSNumber(value: 5)

latitude / longitude

Location 37.7749, -122.4194

Document-Specific Attributes

// For document types attributes.contentType = UTType.pdf attributes.author = "John Doe" attributes.pageCount = 10 attributes.fileSize = 1024000 attributes.path = "/path/to/document.pdf"

Message-Specific Attributes

// For messages attributes.recipients = ["jane@example.com"] attributes.recipientNames = ["Jane Doe"] attributes.authorNames = ["John Doe"] attributes.subject = "Meeting notes"

Batch Indexing for Performance

❌ DON'T: Index items one at a time

// Bad: 100 index operations for order in orders { CSSearchableIndex.default().indexSearchableItems([order.asSearchableItem()]) { _ in } }

✅ DO: Batch index operations

// Good: 1 index operation let items = orders.map { $0.asSearchableItem() }

CSSearchableIndex.default().indexSearchableItems(items) { error in if let error = error { print("Batch indexing error: (error)") } else { print("Indexed (items.count) items") } }

Recommended batch size 100-500 items per call. For larger sets, split into multiple batches.

Deletion Patterns

Delete by Identifier

let identifiers = ["order-1", "order-2", "order-3"]

CSSearchableIndex.default().deleteSearchableItems( withIdentifiers: identifiers ) { error in if let error = error { print("Deletion error: (error)") } }

Delete by Domain

// Delete all items in "orders" domain CSSearchableIndex.default().deleteSearchableItems( withDomainIdentifiers: ["orders"] ) { error in }

Delete All

// Nuclear option: delete everything CSSearchableIndex.default().deleteAllSearchableItems { error in if let error = error { print("Failed to delete all: (error)") } }

When to delete:

  • User deletes content

  • Content expires

  • User logs out

  • App reset/reinstall

App Entity Integration (App Intents)

Create from App Entity

import AppIntents

struct OrderEntity: AppEntity, IndexedEntity { var id: UUID

@Property(title: "Coffee", indexingKey: \.title)
var coffeeName: String

@Property(title: "Date", indexingKey: \.contentCreationDate)
var orderDate: Date

static var typeDisplayRepresentation: TypeDisplayRepresentation = "Order"

var displayRepresentation: DisplayRepresentation {
    DisplayRepresentation(title: "\(coffeeName)", subtitle: "Order from \(orderDate.formatted())")
}

}

// Create searchable item from entity let order = OrderEntity(id: UUID(), coffeeName: "Latte", orderDate: Date()) let item = CSSearchableItem(appEntity: order) CSSearchableIndex.default().indexSearchableItems([item])

Associate Entity with Existing Item

let attributes = CSSearchableItemAttributeSet(contentType: .item) attributes.title = "Order #1234"

let item = CSSearchableItem( uniqueIdentifier: "order-1234", domainIdentifier: "orders", attributeSet: attributes )

// Associate with App Intent entity item.associateAppEntity(orderEntity, priority: .default)

Benefits:

  • Automatic "Find" actions in Shortcuts

  • Spotlight search returns entities directly

  • App Intents integration

NSUserActivity

Overview

NSUserActivity captures user engagement for:

  • Handoff — Continue activity on another device

  • Spotlight search — Index currently viewed content

  • Siri predictions — Suggest returning to this screen

  • Quick Note — Link notes to app content

Platform support iOS 8.0+, iPadOS 8.0+, macOS 10.10+, tvOS 9.0+, watchOS 2.0+, axiom-visionOS 1.0+

Eligibility Properties

let activity = NSUserActivity(activityType: "com.app.viewOrder")

// Enable Spotlight search activity.isEligibleForSearch = true

// Enable Siri predictions activity.isEligibleForPrediction = true

// Enable Handoff to other devices activity.isEligibleForHandoff = true

// Contribute URL to global search (public content only) activity.isEligibleForPublicIndexing = false

Privacy note Only set isEligibleForPublicIndexing = true for publicly accessible content (e.g., blog posts with public URLs).

Basic Pattern

func viewOrder(_ order: Order) { // 1. Create activity let activity = NSUserActivity(activityType: "com.coffeeapp.viewOrder") activity.title = order.coffeeName

// 2. Set eligibility
activity.isEligibleForSearch = true
activity.isEligibleForPrediction = true

// 3. Provide identifier for updates/deletion
activity.persistentIdentifier = order.id.uuidString

// 4. Provide rich metadata
let attributes = CSSearchableItemAttributeSet(contentType: .item)
attributes.title = order.coffeeName
attributes.contentDescription = "Your \(order.coffeeName) order"
attributes.thumbnailData = order.imageData
activity.contentAttributeSet = attributes

// 5. Mark as current
activity.becomeCurrent()

// 6. Store reference (important!)
self.userActivity = activity

}

Critical Maintain strong reference to activity. It won't appear in search without one.

becomeCurrent() and resignCurrent()

// UIKit pattern class OrderDetailViewController: UIViewController { var currentActivity: NSUserActivity?

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    let activity = NSUserActivity(activityType: "com.app.viewOrder")
    activity.title = order.coffeeName
    activity.isEligibleForSearch = true
    activity.becomeCurrent()  // Mark as active

    self.currentActivity = activity
    self.userActivity = activity  // UIKit integration
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    currentActivity?.resignCurrent()  // Mark as inactive
}

}

// SwiftUI pattern struct OrderDetailView: View { let order: Order

var body: some View {
    VStack {
        Text(order.coffeeName)
    }
    .onAppear {
        let activity = NSUserActivity(activityType: "com.app.viewOrder")
        activity.title = order.coffeeName
        activity.isEligibleForSearch = true
        activity.becomeCurrent()

        // SwiftUI automatically manages userActivity
        self.userActivity = activity
    }
}

}

App Intents Integration (appEntityIdentifier)

Connect NSUserActivity to App Intent entities.

func viewOrder(_ order: Order) { let activity = NSUserActivity(activityType: "com.app.viewOrder") activity.title = order.coffeeName activity.isEligibleForSearch = true activity.isEligibleForPrediction = true

// Connect to App Intent entity
activity.appEntityIdentifier = order.id.uuidString

// Now Spotlight can surface this as an entity suggestion
activity.becomeCurrent()
self.userActivity = activity

}

Benefits:

  • Siri suggests this order in relevant contexts

  • App Intents can reference this activity

  • Shortcuts integration

On-Screen Content Tagging

Pattern from WWDC Tag currently visible content for Spotlight parameter suggestions.

func showEvent(_ event: Event) { let activity = NSUserActivity(activityType: "com.app.viewEvent") activity.persistentIdentifier = event.id.uuidString

// Spotlight suggests this event for intent parameters
activity.appEntityIdentifier = event.id.uuidString

activity.becomeCurrent()
userActivity = activity

}

Result When users invoke intents requiring an event parameter, Spotlight suggests the currently visible event.

Quick Note Integration (macOS/iPadOS)

For Quick Note linking, activities must:

  • Be the app's current activity (via becomeCurrent() )

  • Have a clear, concise title (nouns, not verbs)

  • Provide stable, consistent identifiers

  • Support navigation to linked content indefinitely

  • Gracefully handle missing content

let activity = NSUserActivity(activityType: "com.app.viewNote") activity.title = note.title // ✅ "Project Ideas" not ❌ "View Note" activity.persistentIdentifier = note.id.uuidString activity.targetContentIdentifier = note.id.uuidString activity.becomeCurrent()

Activity Continuation (Handling Spotlight Taps)

When users tap Spotlight results, handle continuation:

UIKit

// AppDelegate or SceneDelegate func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { guard userActivity.activityType == "com.app.viewOrder" else { return false }

// Extract identifier
if let identifier = userActivity.persistentIdentifier,
   let orderID = UUID(uuidString: identifier) {
    // Navigate to order
    navigateToOrder(orderID)
    return true
}

return false

}

SwiftUI

@main struct CoffeeApp: App { var body: some Scene { WindowGroup { ContentView() .onContinueUserActivity("com.app.viewOrder") { userActivity in if let identifier = userActivity.persistentIdentifier, let orderID = UUID(uuidString: identifier) { // Navigate to order navigateToOrder(orderID) } } } } }

Searchable Item Continuation

// When continuing from CSSearchableItem func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { if userActivity.activityType == CSSearchableItemActionType { // Get identifier from Core Spotlight item if let identifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String { // Navigate based on identifier navigateToItem(identifier) return true } }

return false

}

Deletion APIs

Delete All Saved Activities

NSUserActivity.deleteAllSavedUserActivities { }

Delete Specific Activities

let identifiers = ["order-1", "order-2"]

NSUserActivity.deleteSavedUserActivities( withPersistentIdentifiers: identifiers ) { }

When to delete:

  • User deletes content

  • User logs out

  • Content no longer accessible

NSUserActivity vs CSSearchableItem

Aspect NSUserActivity CSSearchableItem

Purpose Current user activity Indexing all content

When to use User viewing a screen Background content indexing

Scope One item at a time Batch operations

Handoff Supported Not supported

Prediction Supported Not supported

Search Limited Full Spotlight integration

Example User viewing order detail Index all 500 orders

Recommended Use both:

  • NSUserActivity for screens currently visible

  • CSSearchableItem for comprehensive content indexing

Testing & Debugging

Verify Indexed Items

Using Spotlight

  • Open Spotlight (swipe down on Home Screen)

  • Search for indexed content keywords

  • Verify your app's results appear

  • Tap result → Verify navigation works

Using Console Logs

CSSearchableIndex.default().fetchLastClientState { clientState, error in if let error = error { print("Error fetching client state: (error)") } else { print("Client state: (clientState?.base64EncodedString() ?? "none")") } }

Common Issues

Items not appearing in Spotlight

  • Wait 1-2 minutes for indexing

  • Verify isEligibleForSearch = true

  • Check System Settings → Siri & Search → [App] → Show App in Search

  • Restart device

  • Check console for indexing errors

Activity not triggering Handoff

  • Verify isEligibleForHandoff = true

  • Ensure both devices signed into same iCloud account

  • Check Bluetooth and Wi-Fi enabled on both devices

  • Verify activityType is reverse DNS (com.company.app.action)

Continuation not working

  • Verify application(_:continue:restorationHandler:) implemented

  • Check activityType matches exactly

  • Ensure persistentIdentifier is set

  • Test with debugger to verify method is called

Best Practices

  1. Selective Indexing

❌ DON'T: Index everything

// Bad: Index all 10,000 items let allItems = try await ItemService.shared.all()

✅ DO: Index selectively

// Good: Index recent/important items let recentItems = try await ItemService.shared.recent(limit: 100) let favoriteItems = try await ItemService.shared.favorites()

Why Performance, quota limits, user experience.

  1. Use Domain Identifiers

❌ DON'T: Rely only on unique identifiers

// Hard to delete all orders CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: allOrderIDs)

✅ DO: Group with domains

// Easy to delete all orders CSSearchableIndex.default().deleteSearchableItems(withDomainIdentifiers: ["orders"])

  1. Set Expiration Dates

❌ DON'T: Index items forever

// Bad: Items never expire let item = CSSearchableItem(/* ... */)

✅ DO: Set reasonable expiration

// Good: Expire after 1 year item.expirationDate = Date().addingTimeInterval(60 * 60 * 24 * 365)

  1. Provide Rich Metadata

❌ DON'T: Minimal metadata

attributes.title = "Item"

✅ DO: Rich, searchable metadata

attributes.title = "Medium Latte Order" attributes.contentDescription = "Ordered on December 12, 2025" attributes.keywords = ["coffee", "latte", "order", "medium"] attributes.thumbnailData = imageData

  1. Handle Missing Content Gracefully

func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { guard let identifier = userActivity.persistentIdentifier else { return false }

// Attempt to load content
if let item = try? await ItemService.shared.fetch(id: identifier) {
    navigate(to: item)
    return true
} else {
    // Content deleted or unavailable
    showAlert("This content is no longer available")

    // Delete activity from search
    NSUserActivity.deleteSavedUserActivities(
        withPersistentIdentifiers: [identifier]
    )

    return true  // Still handled
}

}

Complete Example

Comprehensive Integration

import CoreSpotlight import UniformTypeIdentifiers

class OrderManager {

// MARK: - Core Spotlight Indexing

func indexOrder(_ order: Order) {
    let attributes = CSSearchableItemAttributeSet(contentType: .item)
    attributes.title = order.coffeeName
    attributes.contentDescription = "Order from \(order.date.formatted())"
    attributes.keywords = ["coffee", "order", order.coffeeName.lowercased()]
    attributes.thumbnailData = order.thumbnailImageData
    attributes.contentCreationDate = order.date
    attributes.rating = NSNumber(value: order.rating)

    let item = CSSearchableItem(
        uniqueIdentifier: order.id.uuidString,
        domainIdentifier: "orders",
        attributeSet: attributes
    )

    item.expirationDate = Date().addingTimeInterval(60 * 60 * 24 * 365)

    CSSearchableIndex.default().indexSearchableItems([item]) { error in
        if let error = error {
            print("Indexing error: \(error)")
        }
    }
}

func deleteOrder(_ orderID: UUID) {
    // Delete from Core Spotlight
    CSSearchableIndex.default().deleteSearchableItems(
        withIdentifiers: [orderID.uuidString]
    )

    // Delete NSUserActivity
    NSUserActivity.deleteSavedUserActivities(
        withPersistentIdentifiers: [orderID.uuidString]
    )
}

func deleteAllOrders() {
    CSSearchableIndex.default().deleteSearchableItems(
        withDomainIdentifiers: ["orders"]
    )
}

// MARK: - NSUserActivity for Current Screen

func createActivityForOrder(_ order: Order) -> NSUserActivity {
    let activity = NSUserActivity(activityType: "com.coffeeapp.viewOrder")
    activity.title = order.coffeeName
    activity.isEligibleForSearch = true
    activity.isEligibleForPrediction = true
    activity.persistentIdentifier = order.id.uuidString

    // Connect to App Intents
    activity.appEntityIdentifier = order.id.uuidString

    // Rich metadata
    let attributes = CSSearchableItemAttributeSet(contentType: .item)
    attributes.title = order.coffeeName
    attributes.contentDescription = "Your \(order.coffeeName) order"
    attributes.thumbnailData = order.thumbnailImageData
    activity.contentAttributeSet = attributes

    return activity
}

}

// UIKit view controller class OrderDetailViewController: UIViewController { var order: Order! var currentActivity: NSUserActivity?

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    currentActivity = OrderManager.shared.createActivityForOrder(order)
    currentActivity?.becomeCurrent()
    self.userActivity = currentActivity
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    currentActivity?.resignCurrent()
}

}

// SwiftUI view struct OrderDetailView: View { let order: Order

var body: some View {
    VStack {
        Text(order.coffeeName)
            .font(.largeTitle)

        Text("Ordered on \(order.date.formatted())")
            .foregroundColor(.secondary)
    }
    .userActivity("com.coffeeapp.viewOrder") { activity in
        activity.title = order.coffeeName
        activity.isEligibleForSearch = true
        activity.isEligibleForPrediction = true
        activity.persistentIdentifier = order.id.uuidString
        activity.appEntityIdentifier = order.id.uuidString

        let attributes = CSSearchableItemAttributeSet(contentType: .item)
        attributes.title = order.coffeeName
        attributes.contentDescription = "Your \(order.coffeeName) order"
        activity.contentAttributeSet = attributes
    }
}

}

Resources

WWDC: 260, 2015-709

Docs: /corespotlight, /corespotlight/cssearchableitem, /foundation/nsuseractivity

Skills: axiom-app-intents-ref, axiom-app-discoverability, axiom-app-shortcuts-ref

Remember Core Spotlight indexes all your app's content; NSUserActivity marks what the user is currently doing. Use CSSearchableItem for batch indexing, NSUserActivity for active screens, and connect them to App Intents with appEntityIdentifier for comprehensive discoverability.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

axiom-swiftui-architecture

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

axiom-avfoundation-ref

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

axiom-testflight-triage

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

axiom-accessibility-diag

No summary provided by upstream source.

Repository SourceNeeds Review