Spotlight Search Integration for iOS Applications
Spotlight is the system-wide iOS search, accessible by swiping down from the home screen. By indexing application content, users can find it directly in Spotlight without launching the application. Tapping the result is a deep link to the needed screen.
Three APIs for indexing: CSSearchableIndex for user content, NSUserActivity for current activities, CoreSpotlight + AppIntents for Shortcuts integration (iOS 16+).
CSSearchableIndex: The Main API
Each item is a CSSearchableItem with a unique uniqueIdentifier, domainIdentifier (grouping by type), and CSSearchableItemAttributeSet (metadata).
let attributeSet = CSSearchableItemAttributeSet(contentType: .text)
attributeSet.title = article.title
attributeSet.contentDescription = article.summary
attributeSet.keywords = article.tags
attributeSet.thumbnailData = await fetchThumbnail(article.imageURL)
let item = CSSearchableItem(
uniqueIdentifier: "article-\(article.id)",
domainIdentifier: "articles",
attributeSet: attributeSet
)
CSSearchableIndex.default().indexSearchableItems([item]) { error in
if let error { print("Index error:", error) }
}
uniqueIdentifier is the same string you'll receive in application(_:continue:restorationHandler:) when the user taps the result. Use it to determine which screen to open.
Batch indexing. Indexing one item on each load is a bad idea. With a large catalog (thousands of items), use CSSearchableIndex.fetchLastClientState() / beginBatch() / endBatch()—this transactional indexing is faster and doesn't cause unnecessary re-index.
NSUserActivity for Recent Actions
NSUserActivity automatically indexes what the user did in the application—"recently viewed" in Spotlight. Plus, this is the basis for Handoff between devices.
let activity = NSUserActivity(activityType: "com.yourapp.viewArticle")
activity.title = article.title
activity.userInfo = ["articleId": article.id]
activity.isEligibleForSearch = true
activity.isEligibleForHandoff = true
activity.isEligibleForPrediction = true // for Siri Suggestions
activity.becomeCurrent()
Call becomeCurrent() in viewDidAppear, resignCurrent() in viewDidDisappear. Common mistake: calling becomeCurrent() in viewDidLoad—the activity is registered before the user actually sees the content.
Handling Deep Link from Spotlight
In AppDelegate:
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if userActivity.activityType == CSSearchableItemActionType {
let identifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String
// Navigate to item with identifier
}
return true
}
In SwiftUI/SceneDelegate—through the .onContinueUserActivity(CSSearchableItemActionType) modifier.
SceneDelegate migration. On iOS 13+, the application can have multiple scenes. If Spotlight opens the application from scratch—scene(_:willConnectTo:options:) receives userActivity in connectionOptions.userActivities. If the app is already running—scene(_:continue:). Handle both cases.
Deleting and Updating the Index
Outdated content in Spotlight annoys users: they tap—the application opens to a 404. Delete via CSSearchableIndex.default().deleteSearchableItems(withIdentifiers:) or deleteSearchableItems(withDomainIdentifiers:) for an entire type. When deleting user data—delete from the index in the same thread.
Don't index private content without explicit user consent—Apple checks this in review via HIG.
Timeline
Basic indexing of main content: 1–2 weeks. Full integration with NSUserActivity, batch indexing, deep link handling, and index actualization: 3–5 weeks. Cost depends on catalog size and deep link scheme complexity.







