Skip to content
Tolinku
Tolinku
Sign In Start Free
Marketing · · 6 min read

Apple Spotlight Indexing for iOS Apps

By Tolinku Staff
|
Tolinku seo app indexing dashboard screenshot for marketing blog posts

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 webpageURL that points to a real web page.
  • The web page must have a matching Universal Links configuration.
  • The apple-app-site-association file 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:

  1. Running your app on a device.
  2. Navigating to content you want indexed.
  3. Going to the home screen and swiping down to open Spotlight.
  4. Searching for the content title or keywords.
  5. 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.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.