
Donate content to Spotlight and open it using NSUserActivity
Learn how to expose the content of your app to Spotlight.
In this article, we’ll explore how to make your app content discoverable in Spotlight and navigate users directly to detailed views using a combination of CSSearchableItem
and NSUserActivity
.
Previously, we looked at how to achieve this using AppIntents
, specifically with the IndexedEntity
protocol and an OpenIntent
that enables navigation.

This time, we’ll use a different approach that relies on traditional Spotlight integration that complements the AppIntents
and IndexedEntity
strategy we explored earlier, offering a flexible way to support Spotlight search, especially for apps targeting a wide range of iOS versions.
Before we start
To address the topics we are going to leverage on a simple sample project: a book library app that lets you keep track of your books on your shelf.
This starter project contains:
- View folder:
BooksListView
- shows the stored books as a listBookDetailView
- presents the details of a stored bookAddBookView
- handles the addition of a new oneWebView
- rendering the file
- Model folder, collecting all the model files
Book
- defines the book typeDataModel
- data persisted usingSwiftData
- Manager folder
NavigationManager
- handles navigation within the appDataManager
- handles the data operations
- The
BooksShelfCustomIntentApp
file - the entry point of the app - Intents folder - collecting all files related to intents
BookEntity
- handling theAppEntity
for theBook
modelOpenBookIntent
- handling theAppIntent
that allows opening a specific bookShorcutsProviders
- handling theAppShortcutsProvider
conforming type that enables the invocation of the intents via vocal commandsSearchIntent
- handling theAppIntent
that allows to perform research inside your app
- Widget folder - collecting all files related to the Control action.
The NavigationManager
and the shared Modelcontainer
are initialized at the app entry point. Launch the app and start storing your books; they will be needed to launch and test.
Step 1 - Define a Searchable Attribute Set
To make your app’s content discoverable in Spotlight, you first need to create a CSSearchableItemAttributeSet
that specifies the metadata describing each book in Spotlight.
// 1.
extension Book {
// 2.
var attributeSet: CSSearchableItemAttributeSet {
// 3.
let attributes = CSSearchableItemAttributeSet(contentType: .text)
attributes.title = title
attributes.contentDescription = author
return attributes
}
}
- Create an extension of
Book
. - Implement a computed
CSSearchableItemAttributeSet
type, theattributeSet
property feeding it with the metadata the book can be searched by. - Initialize a
CSSearchableItemAttributeSet
object using theinit(contentType:)
to implement attributes that improve search accuracy in Spotlight. Spotlight search uses the title, subtitle, and image from an entity’s display representation. To improve search accuracy and ensure other attributes are searchable, provide a custom attribute set for the data you donate. Here, we’re populating the title and description fields, which Spotlight uses for display and matching queries.
BookEntity
type to IndexedEntity
protocol and leverage the attributeSet
property of a CSSearchableItemAttributeSet
type enabled by the protocol.Step 2 - Index the Item with CSSearchableItem
Once we have our metadata ready, we need to index the item so it becomes searchable.
In DataManager
, we add the indexing logic to the book creation method:
import Foundation
import SwiftData
import CoreSpotlight
@MainActor
class DataManager {
static func collectAllBooks() async throws -> [Book] { ... }
static func collectBooks(for identifiers: [UUID]) async throws -> [Book] { ... }
static func collectBooks(matching string: String) async throws -> [Book] { ... }
static func createNewBook(book: Book) async throws {
...
// 1.
let index = CSSearchableIndex(name: "MyIndex")
// 2.
let searchableItem = CSSearchableItem(
uniqueIdentifier: book.id.uuidString,
domainIdentifier: "antonella.BooksShelfSpotlightNsUserActivity.Book",
attributeSet: book.attributeSet
)
// 3.
try await index.indexSearchableItems([searchableItem])
}
static func deleteBook(book: Book) async throws { ... }
}
- Initialize a
CSSearchableIndex
object, it will handle the on-device index for the app’s searchable content.To initialize the index we used theinit(name:)
that allows to create an index with a name of your choice. One of the advantages of creating a custom index is that you can also specify the type of data protection you wan to apply to the content donated to Spotlight using theinit(name:protectionClass:)
constructor. If there is no need to specify the name of your index, you can use thedefault()
one. - Create a
CSSearchableItem
object. This class handles the details of your app-specific content that someone might search for on their devices: it will uniquely identify the book created, associate it with a domainIdentifier, and set the related metadata that Spotlight will use to look for it later. - When the item is ready, index it using
indexSearchableItems(_:completionHandler:)
method on the index created before.
What we need to do now is updating the index items when an item is deleted.
import Foundation
import SwiftData
import CoreSpotlight
@MainActor
class DataManager {
static func collectAllBooks() async throws -> [Book] { ... }
static func collectBooks(for identifiers: [UUID]) async throws -> [Book] { ... }
static func collectBooks(matching string: String) async throws -> [Book] { ... }
static func createNewBook(book: Book) async throws { ... }
static func deleteBook(book: Book) async throws {
...
// 1.
let index = CSSearchableIndex(name: "MyIndex")
try? await index.deleteSearchableItems(withIdentifiers: [book.id.uuidString])
}
}
- Initialize a
CSSearchableIndex
object, it will handle the on-device index for the app’s searchable content. - Call the
deleteSearchableItems(withIdentifiers:completionHandler:)
method to delete thebook
based on itsid
.
Step 3 - Handle Deep Linking with NSUserActivity
Now that the content is indexed, we want users to tap a result and land directly in the detail view of the selected book. This is where NSUserActivity
comes in.
Update your BooksListView
to respond to NSUserActivity
events triggered by Spotlight:
import SwiftUI
import SwiftData
import CoreSpotlight
struct BooksListView: View {
...
var body: some View {
@Bindable var navigation = navigation
NavigationStack(path: $navigation.navigationPath) {
...
}
...
// 1.
.onContinueUserActivity(CSSearchableItemActionType) { activity in
// 2.
if let bookID = activity.userInfo?[CSSearchableItemActivityIdentifier] as? String, let matchedBook = books.first(where: { $0.id.uuidString == bookID }) {
// 3.
navigation.navigationPath.append(matchedBook)
}
}
}
}
private func deleteBook(at offsets: IndexSet) { ... }
}
- Use the
onContinueUserActivity(_:perform:)
modifier to receiveNSUserActivity
instances. Set theactivityType
parameter on the type of activity that needs to trigger the performing action. In our case is aCSSearchableItemActionType
. - In the closure to be performed when the
activityType
matches with the one specified, check if the activity’suserInfo
contains the identifier of a book. - If it does, append the book to the navigation path, taking the user straight to the detailed view.
Final Result
You can download the final version of the project here:
To recap, to make your app content discoverable and navigable from Spotlight without leveraging the IndexedEntity
, you need to: define a CSSearchableItemAttributeSet
that specifies the metadata describing each item you want to donate to Spotlight; then, you need to create CSSearchableItem
feeding the metadata collected in the CSSearchableItemAttributeSet
; and last, handling deep linking with NSUserActivity
to respond when users tap on Spotlight results and navigate them directly to the corresponding detail view in your app.
This approach gives you full control over how your app’s content integrates with Spotlight and provides a smooth, native experience for users searching on their devices.