Apple Spotlight is the search system built into every iOS device. When users swipe down on their home screen and type a query, Spotlight searches across apps, contacts, messages, files, and the web. If your app indexes its content with Spotlight, your app's content appears in these results, giving users a direct path back into your app.
Unlike Google App Indexing (which surfaces content in web search results), Spotlight indexing is on-device. The index lives locally, which means it is fast, private, and works offline. This guide covers the three indexing methods Apple provides and how to implement each one. For the broader app indexing strategy, see app indexing and SEO for mobile apps. For Google's equivalent, see Google App Indexing setup.
Three Indexing Methods
Apple provides three complementary methods for indexing app content in Spotlight:
| Method | Best For | Scope | Requires Web |
|---|---|---|---|
| CoreSpotlight | App-specific content (products, articles, messages) | On-device only | No |
| NSUserActivity | Content the user has viewed or interacted with | On-device + Apple Search | Yes (for public indexing) |
| Web Markup | Content that exists on your website | Apple Search (cross-device) | Yes |
For maximum coverage, use all three. CoreSpotlight indexes content the user has not viewed yet, NSUserActivity indexes content the user has engaged with (and optionally makes it public), and web markup indexes content from your website.
Method 1: CoreSpotlight
CoreSpotlight lets you create a searchable index of your app's content. Each item you index has a title, description, thumbnail, and a unique identifier that your app uses to route the user to the correct content.
Basic Implementation
import CoreSpotlight
import MobileCoreServices
func indexProduct(_ product: Product) {
let attributeSet = CSSearchableItemAttributeSet(contentType: .content)
attributeSet.title = product.name
attributeSet.contentDescription = product.description
attributeSet.thumbnailData = product.thumbnailData
// Optional: add additional metadata
attributeSet.rating = NSNumber(value: product.averageRating)
attributeSet.ratingDescription = "\(product.reviewCount) reviews"
let item = CSSearchableItem(
uniqueIdentifier: "product-\(product.id)",
domainIdentifier: "com.yourapp.products",
attributeSet: attributeSet
)
// Items expire after 30 days by default
item.expirationDate = Calendar.current.date(byAdding: .day, value: 30, to: Date())
CSSearchableIndex.default().indexSearchableItems([item]) { error in
if let error = error {
print("Indexing failed: \(error.localizedDescription)")
}
}
}
Handling Spotlight Taps
When a user taps a Spotlight result, your app receives the unique identifier. Handle it in your AppDelegate or SceneDelegate:
// SceneDelegate
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
if userActivity.activityType == CSSearchableItemActionType {
guard let identifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String else {
return
}
// Parse the identifier and navigate
if identifier.hasPrefix("product-") {
let productId = String(identifier.dropFirst("product-".count))
navigateToProduct(productId)
}
}
}
Batch Indexing
For large catalogs, index items in batches:
func indexAllProducts(_ products: [Product]) {
let items = products.map { product -> CSSearchableItem in
let attributeSet = CSSearchableItemAttributeSet(contentType: .content)
attributeSet.title = product.name
attributeSet.contentDescription = product.description
return CSSearchableItem(
uniqueIdentifier: "product-\(product.id)",
domainIdentifier: "com.yourapp.products",
attributeSet: attributeSet
)
}
// Index in batches of 100
let batchSize = 100
for batchStart in stride(from: 0, to: items.count, by: batchSize) {
let batchEnd = min(batchStart + batchSize, items.count)
let batch = Array(items[batchStart..<batchEnd])
CSSearchableIndex.default().indexSearchableItems(batch) { error in
if let error = error {
print("Batch indexing failed: \(error.localizedDescription)")
}
}
}
}
Removing Items
When content is deleted from your app, remove it from the Spotlight index:
// Remove a single item
CSSearchableIndex.default().deleteSearchableItems(
withIdentifiers: ["product-123"]
) { error in
// Handle error
}
// Remove all items in a domain
CSSearchableIndex.default().deleteSearchableItems(
withDomainIdentifiers: ["com.yourapp.products"]
) { error in
// Handle error
}
// Remove everything
CSSearchableIndex.default().deleteAllSearchableItems { error in
// Handle error
}
Method 2: NSUserActivity
NSUserActivity was originally designed for Handoff (continuing activities across Apple devices) but also powers Spotlight indexing and Siri suggestions. When you mark an activity as eligible for search, it appears in Spotlight results.
Basic Implementation
class ProductViewController: UIViewController {
var product: Product!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let activity = NSUserActivity(activityType: "com.yourapp.viewProduct")
activity.title = product.name
activity.isEligibleForSearch = true
activity.isEligibleForPublicIndexing = true // Makes it visible to all users
// Required for public indexing: associate with a web URL
activity.webpageURL = URL(string: "https://www.yourapp.com/products/\(product.id)")
// Search attributes
let attributes = CSSearchableItemAttributeSet(contentType: .content)
attributes.contentDescription = product.description
attributes.thumbnailData = product.thumbnailData
activity.contentAttributeSet = attributes
// Keywords for search
activity.keywords = Set(product.tags)
// Link to the app content
activity.userInfo = ["productId": product.id]
self.userActivity = activity
activity.becomeCurrent()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
userActivity?.invalidate()
}
}
Public Indexing
When isEligibleForPublicIndexing is true, Apple may include your content in search results for all users (not just the user who viewed it). This is Apple's equivalent of Google App Indexing.
Requirements for public indexing:
- The activity must have a
webpageURLthat points to a real web page. - The web page must have a matching Universal Links configuration.
- The
apple-app-site-associationfile must be correctly configured on your domain. - Apple must verify that the content is high-quality and relevant.
See the Universal Links guide for the full configuration.
Handling NSUserActivity Taps
When a user taps an NSUserActivity-based Spotlight result:
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
switch userActivity.activityType {
case "com.yourapp.viewProduct":
if let productId = userActivity.userInfo?["productId"] as? String {
navigateToProduct(productId)
}
case CSSearchableItemActionType:
// CoreSpotlight result (handled separately)
break
default:
// Check for Universal Link
if let url = userActivity.webpageURL {
handleUniversalLink(url)
}
}
}
Method 3: Web Markup
If your content exists on a website, you can use Apple's web markup to associate web pages with your app. This requires no code changes in your app (only website changes), but it depends on having Universal Links configured.
Smart App Banner
Add a Smart App Banner meta tag to your web pages:
<meta name="apple-itunes-app" content="app-id=YOUR_APP_STORE_ID, app-argument=https://www.yourapp.com/products/123">
The app-argument is the URL that your app receives when the user taps "Open" on the banner. Your app must handle this URL via Universal Links.
Open Graph and Schema.org
Apple's crawler reads standard web markup:
<head>
<meta property="og:title" content="Product Name" />
<meta property="og:description" content="Product description" />
<meta property="og:image" content="https://www.yourapp.com/images/product-123.jpg" />
<meta property="og:url" content="https://www.yourapp.com/products/123" />
</head>
Combined with a working Universal Links setup, this markup helps Apple associate your web content with your app.
Best Practices
Index Strategically
Do not index everything. Focus on content that users are likely to search for:
- Product names and descriptions.
- Article titles and summaries.
- Contact names (for messaging apps).
- Location names (for maps/travel apps).
- Media titles (for entertainment apps).
Avoid indexing transient content (notifications, loading screens, settings pages).
Keep the Index Fresh
Stale results frustrate users. Update the index when content changes:
func updateProduct(_ product: Product) {
// Re-index with updated data
indexProduct(product)
}
func deleteProduct(_ productId: String) {
CSSearchableIndex.default().deleteSearchableItems(
withIdentifiers: ["product-\(productId)"]
)
}
Use Rich Attributes
CoreSpotlight supports many attribute types:
let attributes = CSSearchableItemAttributeSet(contentType: .content)
attributes.title = "Restaurant Name"
attributes.contentDescription = "Italian restaurant in downtown"
attributes.thumbnailData = thumbnailData
attributes.rating = 4.5
attributes.ratingDescription = "4.5 stars (230 reviews)"
attributes.phoneNumbers = ["+1-555-123-4567"]
attributes.supportsPhoneCall = true
attributes.latitude = 37.7749
attributes.longitude = -122.4194
Rich attributes make your Spotlight results more informative and actionable.
Test on Device
Spotlight indexing only works on physical devices (not the simulator for some features). Test by:
- Running your app on a device.
- Navigating to content you want indexed.
- Going to the home screen and swiping down to open Spotlight.
- Searching for the content title or keywords.
- Tapping the result and verifying navigation.
Tolinku and Spotlight Indexing
Tolinku handles the Universal Links infrastructure that powers Apple's public indexing. When you configure your custom domain with Tolinku, the platform automatically serves the apple-app-site-association file, enabling both Universal Links and the web-based Spotlight indexing path.
For Google's equivalent, see Google App Indexing setup. For the broader strategy, see app indexing and SEO for mobile apps.
Get deep linking tips in your inbox
One email per week. No spam.