Implement Core Spotlight in a SwiftUI App

Essentials to advanced

Yugantar Jain
Better Programming

--

Photo by Markus Winkler on Unsplash

Spotlight is the powerful search system on Apple’s devices that we all love to use to quickly find our content from our favourite apps and interact with them.

By using the Core Spotlight API, your app can also harness the power of this powerful search engine and make its content available for quick and easy access (and much more). Making your app’s content available on Spotlight is not only a great experience for the users, but also immediately increases the presence of your app manifold.

This is a comprehensive and simple guide that will help you to harness the full power of Spotlight search on Apple devices using the Core Spotlight API in your SwiftUI app.

We’ll discuss everything from the essentials of implementation to advanced use-cases including:

  1. Indexing: show your app data in the Spotlight search
  2. Deleting: remove your app data from the Spotlight search
  3. Handling: handle the user’s tap on a Spotlight search item
  4. Continuing the Spotlight search in-app
  5. Custom actions for Spotlight search items
  6. Custom preview for Spotlight search items

Please note: this article is not for using Core Spotlight to index Core Data items. If that is your use-case, I’d recommend watching this session:

Now let’s get started!

1) Indexing: show your app data in Spotlight search

Let’s start with a fresh sample project.

  • First of all, to use the Core Spotlight API, we need to import it:
import CoreSpotlight
  • Next, let’s define some sample app data that we’ll show in the app and the Spotlight search. Your code would look something like this:
import SwiftUI
import CoreSpotlight
struct ContentView: View {
// Sample app-data
let appData = [102, 503, 653, 998, 124, 587, 776, 354, 449, 287]
var body: some View {
}
}
  • Now, let’s create a basic UI that’ll show our app data in a list. We’ll also create a button that will index the data.

Our groundwork is set now. However, if you’ll click on the ‘Index Data’ button nothing will happen because the indexData() method is empty right now. It’s time to write the code that will index our data and automatically show it in the Spotlight search now!

To index the data, Core Spotlight uses a special object called CSSearchableItem. Our app-data is an array of 10 elements, hence to index them we’ll have to create 10 corresponding CSSearchableItems. So, let’s start off by creating an array for it.

func indexData() {
var searchableItems = [CSSearchableItem]()
}

We control what data is presented in Spotlight search for a specific element by setting its attributes. This is once again done using a specialized object called CSSearchableItemAttributeSet. A basic implementation would be:

appData.forEach {
let attributeSet = CSSearchableItemAttributeSet(contentType: .content)
attributeSet.displayName = $0.description
}

Lastly, we use this attribute set to create the CSSearchableItem object, appending to the array we defined earlier, and finally submitting it for indexing. And voila, we’re done!

So in the end, the indexData() method would look something like this:

func indexData() {
var searchableItems = [CSSearchableItem]()

appData.forEach {
// Set attributes
let attributeSet = CSSearchableItemAttributeSet(contentType: .content)
attributeSet.displayName = $0.description

// Create searchable item
let searchableItem = CSSearchableItem(uniqueIdentifier: nil, domainIdentifier: "sample", attributeSet: attributeSet)
searchableItems.append(searchableItem)
}

// Submit for indexing
CSSearchableIndex.default().indexSearchableItems(searchableItems)
}

While most of it is straightforward, I’d like to explain the creation of theCSSearchableItem object a bit more.

The CSSearchableItem initializer has three parameters:

  1. uniqueIdentifier: this is used to give a unique id to the element being indexed. While this is an optional property, it is recommended you set it since it is also used to handle user interaction by identifying the item.
  2. domainIdentifier: this is used to group the data in a meaningful way. For example: a music app may use domain ids like ‘songs’, ‘albums’, etc. While this is an optional property, it is usually recommended to set it.
  3. attributeSet: we pass the CSSearchableItemAttributeSet object that we created here.

Our code for indexing the app data in Spotlight search is functional now. If we click on the ‘Index Data’ button and then search in Spotlight, we should be able to see our app’s content that we indexed in the results:

We can play with the attribute set to change the data presented for our app’s results in Spotlight. For example, if we set the thumbnailData and phoneNumbers property of our attributeSet, the search results look like:

2. Deleting: remove your app data from Spotlight search

Deleting/removing the app data from Spotlight index is fairly straightforward:

func deleteData() {
CSSearchableIndex.default()
.deleteSearchableItems(withDomainIdentifiers: ["sample"])
}

If you’ll start typing the code in Xcode, you’ll realise (thanks to auto-complete) that there are multiple ways to delete the searchable items.

Above is an example where you can delete the searchable items for a particular set of domain identifiers. Since we used the same domain identifier (“sample”) while indexing, this will delete all the indexed items in our sample app.

3. Handling: handle the user’s tap on a Spotlight search item

Whenever a user will tap on a Spotlight search result of your app, the system will notify that along with some useful information through NSUserActivity.

To enable your app to get the relevant NSUserActivity, SwiftUI has a modifier called onContinueUserActivity. Apply this on your main view like this:

.onContinueUserActivity(CSSearchableItemActionType, perform: handleSpotlight)

CSSearchableItemActionType is a key that is part of the Core Spotlight API (hence make sure to import it). This makes sure that the handleSpotlight method only receives the NSUserActivity for Spotlight interaction.

Bonus tip: To get multiple user activities, you’ll apply this modifier multiple times with the respective keys.

Now let’s see what goes into the handleSpotlight method:

func handleSpotlight(userActivity: NSUserActivity) {
guard let uniqueIdentifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String else {
return
}

// Handle spotlight interaction
// Maybe deep-link, or something else entirely
// This totally depends on your app's use-case
print("Item tapped: \(uniqueIdentifier)")
}

Using the NSUserActivity object that you receive here, you can retrieve the uniqueIdentifier that you passed while creating the CSSearchableItem object while indexing data.

This way you can identify the element tapped and then handle it as per your app’s use case.

In the attached GitHub project, the most common handling scenario of navigation to the detail view for the selected element is implemented for your reference.

4. Continuing the Spotlight search in-app

We can continue the user’s search in Spotlight in our own app.

Display the button

First of all, we need to show the `Search in App` button in the Spotlight search for our app’s results. To do this, we simply need to add the CoreSpotlightContinuation key and set it to YES in our Info.plist like this:

Enable `Search in App` in our Spotlight results

After that the `Search in App` button must appear in our Spotlight results.

Please note: you might need to restart your device for the `Search in App` button to appear.

Handle the input

When the user taps on the `Search in App` button, our app launches to the foreground and receives the user’s search string which we can then use to continue the search in-app.

The app receives the spotlight search string as an NSUserActivity, so to retrieve and handle it we can use the onContinueUserActivity modifier. Apply it on the main view in your app like this:

.onContinueUserActivity(CSQueryContinuationActionType, perform: handleSpotlightSearchContinuation)

Here, we get the search string using the Core Spotlight API’s predefined key CSQueryContinuationActionType and handle it in our method handleSpotlightSearchContinuation.

Inside our method, we can retrieve the search string and use it as per the app’s architecture and use-case like this:

func handleSpotlightSearchContinuation(userActivity: NSUserActivity) {
guard let searchString = userActivity.userInfo?[CSSearchQueryString] as? String else {
return
}

// Continue spotlight search
// Use the search string as per your app's use-case
print(searchString)
}

5) Custom actions for Spotlight search items

We can define custom actions for our Spotlight search items. These custom actions can be accessed by the user by long-pressing on the respective Spotlight search item.

Display the custom actions

To display the custom actions in our spotlight search results we need to do two things.

First, we need to add the custom actions in the Info.plist of our app. We can set the title string, icon (using SF Symbol), and an identifier string.

Add custom spotlight actions in info.plist

Next, we need to pass the identifiers for the custom actions we set here to the CSSearchableItem while indexing data.

So in our indexData method, we’ll add the following line:

attributeSet.actionIdentifiers = [“CS_ACTION_1”, “CS_ACTION_2”]

Now, when we long-press on our Spotlight search item, we should see our custom actions.

Please note: if your data is already indexed, you will need to delete it from Spotlight and re-index it for the custom actions to show up.

Handle the custom actions

We handle the custom actions using the same NSUserActivitiy when a spotlight search item of our app is tapped.

So we can modify the handleSpotlight method we defined earlier to also handle our custom actions. The final version will look like this:

func handleSpotlight(userActivity: NSUserActivity) {
// Get selected spotlight search item
guard let element = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String else {
return
}

// If custom action requested, execute it
if let actionIdentifier = userActivity.userInfo?[CSActionIdentifier] as? String {
if actionIdentifier == "CS_ACTION_1" {
// Perform action 1
} else if actionIdentifier == "CS_ACTION_2" {
// Perform action 2
}
}
// Else handle the element
// Maybe deep-link, or something else entirely
// This totally depends on your app's use-case
else {
self.selection = Int(element)
}
}

In the method above, we first make sure to retrieve the element tapped.

After that we check if a custom action was tapped by checking for the actionIdentifier value. If we get a value, we can perform the appropriate action. If not, we simply handle the tapped element.

6) Custom preview for Spotlight search items

When we long press our Spotlight search items to access the custom actions, we also see an automatic preview.

This automatically generated preview is quite basic, however, we can replace this preview with our own view.

  1. First of all, we need to add the Quick Look Preview Extension target to our app. Go to File > New > Target to add it and give it a name.
Add Quick Look Preview Extension target

After adding it, Xcode will prompt to activate it for debugging purposes. Click on `Activate` and we are good to go.

2. Next, find Info.plist inside the target and set the key QLSupportsSearchableItems to YES.

Enable Quick Look previews for Spotlight items

3. Strictly speaking for custom previews for Spotlight search items, we don’t need the PreviewProvider.swift file in the Target and can go ahead and delete it.

4. In the PreviewViewController.swift file, uncomment the preparePreviewOfSearchableItem method. This is the method where we’ll configure our custom preview. We won’t need the preparePreviewOfFile method and can go ahead and delete it.

class PreviewViewController: UIViewController, QLPreviewingController {  
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}

func preparePreviewOfSearchableItem(identifier: String, queryString: String?, completionHandler handler: @escaping (Error?) -> Void) {
// Perform any setup necessary in order to prepare the view.

// Call the completion handler so Quick Look knows that the preview is fully loaded.
// Quick Look will display a loading spinner while the completion handler is not called.
handler(nil)
}
}

5. The custom preview UI is shown using the MainInterface.storyboard in the target.

We can modify the Storyboard to get the desired custom preview and configure it in the preparePreviewOfSearchableItem method using the parameters:

  • identifier (that we set for each item while indexing)
  • queryString (spotlight search string)

For example:

Example configuration of MainInterface.storyboard

Now when we long press on a Spotlight search item of our app, we get a custom preview like this:

Custom Preview for our app’s Spotlight search item

Pro-tip: use a package if you want to share data/logic between your app and the Quick Look extension.

For example, to retrieve additional information about the object using the identifier.

Thank you for reading! I hope you found this post helpful, simple, and interesting.

The complete sample project can be found here for reference.

--

--