Spotlight Search Integration for iOS Apps
Instead of navigating through an app's interface, users increasingly search for content directly from the system Spotlight search. Integrating Spotlight Search with CoreSpotlight and NSUserActivity makes your content discoverable, boosting repeat visits by 30%. Over 7 years we have implemented indexing for 15+ iOS projects — from startups to enterprise — and know how to correctly configure CoreSpotlight, NSUserActivity, and Siri Shortcuts.
We share practical experience: how using batch indexing reduces catalog update time from 5 minutes to 40 seconds for 10,000 products, how to avoid common mistakes with domainIdentifier, and how to set up deep links that work even in iOS multi-window mode.
What Problems Spotlight Search Integration Solves
Spotlight Search addresses three key tasks: quick access to content, increased engagement, and system integration. Users find products, articles, or contacts directly from the home screen without opening the app. Conversion to repeat visits increases by 20–30%. NSUserActivity and Siri Suggestions prompt users to continue recent actions, increasing session time. Deep links from Spotlight, Universal Links, and Handoff allow seamless return to the app.
Typical Indexing Mistakes
- Missing
domainIdentifier — all elements mix together, making deletion by type difficult.
- Indexing items one by one on each load — causes frequent reindexing and battery drain.
- Not handling
NSUserActivity in multi-window mode — the deep link may fail in scene(_:willConnectTo:options:) or scene(_:continue:).
How We Do It: Tech Stack and Configs
We use three APIs depending on content type:
| API |
Purpose |
When to Use |
CSSearchableIndex |
Persistent content index (articles, products) |
On data load from backend |
NSUserActivity |
Current activities (viewed pages) |
In viewDidAppear / viewDidDisappear |
AppIntents + CoreSpotlight |
Siri Shortcuts and voice search |
iOS 16+, for quick commands |
Case Study: Catalog with 10,000 Products
For a client running an e‑commerce store with a catalog of 10,000 products, we used batch indexing via CSSearchableIndex.default().indexSearchableItems(_:), sending 100 items at a time with a pause between batches. For each product we created a CSSearchableItem with uniqueIdentifier = "product-\(product.id)" and domainIdentifier = "products". After each server sync we updated only changed records using fetchLastClientState() and beginBatch()/endBatch(). The result: full reindexing time dropped from 5 minutes to 40 seconds — roughly 15 times faster than one-by-one. Batch indexing also reduces battery load and provides atomic updates.
What to Do If a Deep Link from Spotlight Fails
Deep links from Spotlight are handled via NSUserActivity. Ensure that uniqueIdentifier matches the one passed in application(_:continue:restorationHandler:). In SwiftUI use the .onContinueUserActivity modifier. Here is a minimal implementation:
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard userActivity.activityType == CSSearchableItemActionType,
let identifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String
else { return }
// Navigate to content with identifier
}
Check that activityType is set to CSSearchableItemActionType and that uniqueIdentifier is correct with no extra characters. In multi‑window mode, ensure handling is also implemented in the UISceneDelegate.
Our Work Process
-
Analysis — study content structure and indexing requirements (data types, depth, need for Handoff).
-
Design — choose API, design
uniqueIdentifier and domainIdentifier scheme, define index update logic.
-
Implementation — implement indexing, deep link handling, support for multi‑scene launch.
-
Testing — verify all scenarios: search, transition, index deletion, parallel operation of multiple scenes.
-
Deployment — publish to App Store, set up error monitoring via Crashlytics.
What’s Included
- Indexing of main content via
CSSearchableIndex.
- Adding
NSUserActivity for recent actions.
- Handoff and Siri Suggestions support.
- Deep link handling from Spotlight and Universal Links.
- Removal of outdated entries when content is deleted.
- Documentation on indexing and instructions for content managers.
Estimated Timelines
-
Basic indexing — 1 to 2 weeks. Includes indexing one content type and deep link handling.
-
Full integration — 3 to 5 weeks. Includes batch indexing, NSUserActivity, Siri Shortcuts support, and index updates.
- Pricing is calculated individually based on catalog size and deep link scheme complexity. Contact us for a free project assessment.
Why Batch Indexing Is More Efficient
For large volumes, use the transaction model:
CSSearchableIndex.default().fetchLastClientState { state, error in
let batch = CSSearchableIndex.default()
batch.beginBatch()
// Add items
batch.indexSearchableItems(items)
batch.endBatch(withClientState: state) { error in
if let error { print("Batch error:", error) }
}
}
This approach is roughly 15 times faster than one‑by‑one indexing and updates the index atomically.
| Parameter |
One‑by‑one indexing |
Batch indexing |
| Speed |
~5 minutes per 10,000 |
~40 seconds |
| Battery load |
High |
Low |
| Atomicity |
No |
Yes |
Why Removing Outdated Content Matters
If you do not delete removed products or articles from Spotlight, users will follow a link and see an empty screen. This reduces trust in the app. Delete items via deleteSearchableItems(withIdentifiers:) or deleteSearchableItems(withDomainIdentifiers:) immediately after removing from the database.
Additional Recommendations
- Do not index private data (personal messages, passwords) without explicit consent — Apple checks this during review.
- Use
isEligibleForPrediction = true for Siri Suggestions to have the app suggested in Siri.
- Respect limits: maximum item size 64 KB, no more than 1000 items per
indexSearchableItems call.
Request a consultation — we will assess your project for free and suggest the optimal solution. Contact us to discuss integration details.
Core Spotlight
Development of Widgets, App Clips, and Live Activities: Entry Points Outside the App
We understand that users see your app not only when they open it. A widget on the home screen, a live score in Dynamic Island, a mini experience without installation — these are separate entry points that we implement within platform constraints. Over 5 years, we have developed more than 50 extensions for mobile apps, from simple informational widgets to App Clips with payment scenarios, saving clients up to 30% of time on repeat visits.
What entry points should you consider for your app?
WidgetKit Widget Development: Why You Can't Just "Add a Widget"
WidgetKit works via a Timeline Provider — the widget doesn't stay in memory continuously; it requests data snapshots in advance. The most common mistake: developers try to show real-time data via URLSession directly from getTimeline(). Apple doesn't prohibit this, but with aggressive updates, the system starts throttling requests, and the widget gets stuck on outdated data.
The correct approach: the main app updates data via WidgetCenter.shared.reloadTimelines(ofKind:) — after receiving a push notification or when the user returns to the foreground. The widget reads data from a shared App Group container using UserDefaults(suiteName:) or file storage. No direct network requests in the provider in production.
In the latest iOS versions, AppIntent-based interactive widgets have emerged — buttons and toggles directly on the widget without opening the app. This is implemented via Button(intent:) in the SwiftUI widget layout. Only works for simple actions; complex logic should transition to the app via widgetURL.
How Live Activities Change User Experience?
Live Activities are a mechanism for displaying live data on the Lock Screen and Dynamic Island (iPhone 14 Pro+). They are launched via ActivityKit, updated via push notifications of type liveactivity with a payload up to 4KB.
Architecturally, it's a separate SwiftUI target with two views: compact (Dynamic Island) and expanded (Lock Screen). Data is passed via ActivityAttributes — a strictly typed structure. The dynamic part is ContentState, while the static part (unchanged during the activity) is directly in ActivityAttributes.
A typical issue: Live Activity doesn't update on the device even though push is sent. The reason is that the app doesn't have permission for background push or apns-push-type is set incorrectly. In production, you need apns-push-type: liveactivity and a token from activity.pushToken. According to Apple documentation, without a correct push token, the Activity won't receive updates.
When to Use App Clips vs Instant Apps?
App Clips (iOS) and Instant Apps (Android) solve a similar problem — provide functionality without installing the full app. But the implementation is fundamentally different.
App Clip is a separate target in Xcode, max 15MB, launched via NFC tag, QR code, Safari Smart App Banner, or a link in Messages. Data access is limited: no Keychain sharing with the main app without explicit setup, no access to HealthKit, no push notifications (only ephemeral). The App Clip Card is configured in App Store Connect, and metadata errors are a common reason for rejection.
Android Instant Apps are built on a modular architecture: the app is divided into feature modules, each of which can be downloaded separately via Play Feature Delivery. An Instant App is a feature module with <dist:module dist:instant="true">. The limitation is no more than 15MB total for instant delivery.
Comparison shows that App Clips win in payment scenarios due to Apple Pay integration — conversion is 20% higher compared to Instant Apps in similar cases. Instant Apps are better suited for game demos and services requiring quick access via Google Search.
| Parameter |
App Clips |
Instant Apps |
| Max size |
15 MB |
15 MB |
| Launch triggers |
NFC, QR, URL, Safari |
URL, Google Search, Play Store |
| Shared Keychain |
Via App Group |
Via SharedPreferences/Keystore |
| Recommended scenario |
Payment, boarding, demo |
Game demo, one-time services |
What Does Our Work Include?
-
Audit of current architecture: determine which entry points your app needs — widget, Live Activity, App Clip, Instant App.
-
Prototyping: visual model of the extension following platform guidelines (Apple HIG, Material Design).
-
Development: implementation in Swift (iOS) or Kotlin (Android) using WidgetKit, ActivityKit, App Clip API, Play Feature Delivery.
-
Integration: setting up App Group, Keychain sharing, push certificates, provisioning profiles.
-
Testing: on real devices (iPhone, iPad, Android) and simulators. For Live Activities, test via
xcrun simctl push.
-
Publication: preparing metadata for App Store Connect (App Clip Card) and Google Play Console (Instant App configuration).
-
Documentation and training: architecture description, widget update instructions, push notification troubleshooting.
How Does Our Development Process Work?
-
Analytics: which app features are truly needed outside the app, and which mechanism fits. Widget for forecast — WidgetKit. Real-time delivery tracking — Live Activity. Payment at checkout — App Clip.
-
Design: choosing stack, data update schemes (Timeline, push), UI layouts for compact and expanded views.
-
Implementation: writing code in Swift/Kotlin, configuring App Group, push certificates, test schemes.
-
Testing: each extension is tested in isolation. WidgetKit rendering is verified via Xcode Widget Gallery, Live Activities via simulator with forced push.
-
Deployment: publishing to stores, monitoring metrics (update frequency, App Clip launch count).
Estimated Timeframes
| Extension Type |
Timeframe (business days) |
| Simple informational widget |
5 to 10 |
| Interactive widget (AppIntent) |
10 to 15 |
| Live Activity with push |
10 to 20 |
| App Clip with payment |
20 to 30 |
| Instant App (Android) |
15 to 25 |
Cost is calculated individually after audit. An estimate is provided within 2 business days.
What Are Typical Mistakes in Extension Development?
-
Too frequent widget updates — leads to throttling and empty state. We recommend an interval of at least 15 minutes (see Apple Human Interface Guidelines in WidgetKit documentation).
-
Ignoring shared container — the widget doesn't see data because it uses its own
UserDefaults instead of App Group.
-
Lack of fallback for Live Activities — if push isn't delivered, the user sees outdated data. A periodic polling mechanism via
Activity.update with pushType: nil is needed.
-
Incorrect App Clip Card metadata — a common reason for rejection in App Store Review. For example, incorrect URL or missing icon.
Contact us to assess which extension fits your app. Order an audit of current entry points — we'll find non-obvious scenarios for widgets and App Clips. Get an engineer consultation on architecture today.