Adding your app’s content to Spotlight

Published on: March 23, 2020

On iOS, you can swipe down on the home screen to access the powerful Spotlight search feature. Users can type queries in Spotlight and it will search through several areas of the system for results. You may have noticed that Spotlight includes iMessage conversations, emails, websites, and more. As an app developer, you can add content from your app to the Spotlight search index so your users can find results that exist in your app through Spotlight.

An important aspect of the Spotlight index is that you can choose whether you want to index your app contents publicly, or privately. In this post, you will learn what that means and how it works.

All in all, this post covers the following topics:

  • Adding content to Spotlight
  • Adding Spotlight search as an entry point for your app

By the end of this post, you will know everything you need to know to add your app's content to the iOS Spotlight index to enhance your app's discoverability.

Adding content to Spotlight

There are several mechanisms that you can utilize to add content to Spotlight. I will cover two of them in this section:

  • NSUserActivity
  • CSSearchableItem

For both mechanisms, you can choose whether your content should be indexed publicly, or privately. When you index something privately, the indexed data does not leave the user's device and it's only added to your user's Spotlight index. When you choose to index an item publicly, a hash of the indexed item is sent to Apple's servers. When other user's devices start sending the same hash to Apple's servers, Apple will begin recognizing your indexed item as useful, or important. Note that having many users send the same item once doesn't mean much to Apple. They are looking for indexes items that are accessed regularly by each user.

I don't know the exact thresholds Apple maintains, but once a certain threshold is reached, Apple will add the publicly indexed item to Spotlight for users that have your app but may not have accessed the content you have indexed for other users. If your indexed items include a Universal Link URL, your indexed item can even appear in Safari's search results if the user doesn't have your app installed yet. This means that adding content to the Spotlight index and doing so accurately and honestly, can really boost your app's discoverability because you might appear in places on a user's where you otherwise would not have.

Adding content to Spotlight using NSUserActivity

The NSUserActivity class is used for many activity related objects in iOS. It's used for Siri Shortcuts, to encapsulate deeplinks, to add content to Spotlight and more. If you're familiar with NSUserActivity, the following code should look very familiar to you:

let userActivity = NSUserActivity(activityType: "com.donnywals.example")
userActivity.title = "This is an example"
activity.persistentIdentifier = "example-identifier"
userActivity.isEligibleForSearch = true // add this item to the Spotlight index
userActivity.isEligibleForPublicIndexing = true // add this item to the public index
userActivity.becomeCurrent() // making this the current user activity will index it

As you can see, creating and indexing an NSUserActivity object is relatively straightforward. I've only used a single attribute of the user activity. If you want to index your app's content, there are several other fields you might want to populate. For example, contentAttributeSet, keywords and webpageURL. I strongly recommend that you look at these properties in the documentation for NSUserActivity and populate them if you can. You don't have to though. You can use the code I've shown you above and your indexed user activities should pop up in Spotlight pretty much immediately.

User activities are ideally connected to the screens a user visits in your app. For instance, you can register them in viewDidAppear and set the created user activity to be the view controllers activity before calling becomeCurrent:

self.userActivity = userActivity
self.userActivity?.becomeCurrent()

You should do this every time your user visits the screen that the user activity belongs to. By doing this, the current user activity is always the true current user activity, and iOS will get a sense of the most important and often used screens in your app. This will impact the Spotlight search result ranking of the indexed user activity. Items that are used regularly rank higher than items that aren't used regularly.

Adding content to Spotlight using CSSearchableItem

A CSSearchableItem is typically not connected to a screen like user activities are. Ultimately a CSSearchableItem of course belongs to some kind screen, but what I mean is that the moment of indexing a CSSearchableItem is not always connected to a user visiting a screen in your app. If your app has a large database of content, you can use CSSearchableItem instances to index your content in Spotlight immediately.

Attributes for a CSSearchableItem are defined in a CSSearchableItemAttributeSet. An attribute set can contain a ton of metadata about your content. You can add start dates, end dates, GPS coordinates, a thumbnail, rating and more. Depending on the fields you populate, iOS will render your indexed item differently. When you add content to Spotlight, make sure you provide as much content as possible. For a full overview of the properties that you can set, refer to Apple's documentation. You can assign an attribute set to the contentAttributeSet property on NSUserActivity to make it as rich as a CSSearchableItem is by default.

You can create an instance of CSSearchableItemAttributeSet as follows:

import CoreSpotlight // don't forget to import CoreSpotlight at the top of your file
import MobileCoreServices // needed for kUTTypeText

let attributes = CSSearchableItemAttributeSet(itemContentType: "com.donnywals.favoriteMovies")
attributes.title = indexedMovie.title
attrs.contentType = kUTTypeText as String
attributes.contentDescription = indexedMovie.description
attributes.identifier = indexedMovie.id
attributes.relatedUniqueIdentifier = indexedMovie.id

In this example, I'm using a made-up indexedMovie object to add to the Spotlight index. I haven't populated a lot of the fields that I could have populated because I wanted to keep this example brief. The most interesting bits here are the identifier and the relatedUniqueIdentifier. Because you can index items through both NSUserActivity and CSSearchableItem, you need a way to tell Spotlight when two items are really the same item. You can do this bu setting the searchable attributes' relatedUniqueIdentifier to the same value you'd use for the user activity's persistentIdentifier property. When Spotlight discovers a searchable item whose's attributes contain a relatedUniqueIdentifier that corresponds with a previously indexed user activity's persistentIdentifier, Spotlight will know to not re-index the item but instead, it will update the existing item.

Important!:
When you add a new item to Spotlight, make sure to assign a value to contentType. In my tests, the index does not complain or throw errors when you index an item without a contentType, but the item will not show up in the Spotlight index. Adding a contentType fixes this.

Once you've prepared your searchable attributes, you need to create and index your searchable item. You can do this as follows:

let item = CSSearchableItem(uniqueIdentifier: "movie-\(indexMovie.id)", domainIdentifier: "favoriteMovies", attributeSet: attributes)

The searchable item initializer takes three arguments. First, it needs a unique identifier. This identifier needs to be unique throughout your app so it should be more specialized than just the identifier for the indexed item. Second, you can optionally pas a domain identifier. By using domains for the items you index, you can separate some of the indexed data which will allow you to clear certain groups of items from the index if needed. And lastly, the searchable attributes are passed to the searchable item. To index the item, you can use the following code:

CSSearchableIndex.default().indexSearchableItems([item], completionHandler: { error in
  if let error = error {
    // something went wrong while indexing
  }
})

Pretty straightforward, right? When adding items to the Spotlight index like this, make sure you add the item every time the user interacts with it. Similar to user activities, iOS will derive importance from the way a user interacts with your indexed item.

Note that we can't choose to index searchable items publicly. Public indexing is reserved for user activities only.

When you ask Spotlight to index items for your app, the items should become available quickly after indexing them. Try swiping down on the home screen and typing the title of an item you've indexed. It should appear in Spotlights search results and you can tap the result to go to your app. However, nothing really happens when your app opens. You still need a way to handle the Spotlight result so your user is taken to the screen in your app that displays the content they've tapped.

Showing the correct content when a user enters your app through Spotlight

Tip:
I'm going to assume you've read my post on handling deeplinks or that you know how to handle deeplinks in your app. A lot of the same principles apply here and I want to avoid explaining the same thing twice. What's most important to understand is which SceneDelegate and AppDelegate methods are called when a user enters your app via a deeplink, and how you can navigate to the correct screen.

In this section, I will only explain the Spotlight specific bits of opening a user activity or searchable item. The code needed to handle Spotlight search items is very similar to the code that handles deeplinks so your knowledge about handling deeplinks carries over to Spotlight nicely.

Your app can be opened to handle a user activity or a searchable item. How you handle them varies slightly. Let's look at user activity first because that's the simplest one to handle.

When your app is launched to handle any kind of user activity, the flow is the same. The activity is passed to scene(_:continue:) if your app is already running in the background, or through connectionOptions.userActivities in scene(_:willConnectTo:options:) if your app is launched to handle a user activity. If you're not using the SceneDelegate, your AppDelegate's application(_:continue:restorationHandler:) method is called, or the user activity is available through UIApplicationLaunchOptionsUserActivityKey on the application's launch options.

Once you've obtained a user activity, it's exposed to you in the exact same way as you created it. So for the user activity I showed you earlier, I could use the following code to handle the user activity in my scene(_:continue:) method:

func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
  if userActivity.activityType == "com.donnywals.example",
    let screenIdentifier = userActivity.persistentIdentifier {

    // navigate to screen
  }
}

In my post on handling deeplinks I describe some techniques for navigating to the correct screen when handling a deeplink, and I also describe how you can handle a user activity from scene(_:willConnectTo:options:). I recommend reading that post if you're not sure how to tackle these steps because I want to avoid explaining the same principle in two posts.

When your app is opened to handle a spotlight item, it will also be asked to handle a user activity. This user activity will look slightly different. The user activity's acitivityType will equal CSSearchableItemActionType. Furthermore, the user activity will not expose any of its searchable attributes. Instead, you can extract the item's unique identifier that you passed to the CSSearchableItem initializer. Based on this unique identifier you will need to find and initialize the content and screen a user wants to visit. You can use the following code to detect the searchable item and extract its unique identifier:

if userActivity.activityType == CSSearchableItemActionType,
  let itemIdentifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String {

  // handle item with identifier
}

Again, the steps from here are similar to how you'd handle a deeplink with the main difference being how you find content. If you're using Core Data or Firebase, you will probably want to use the item identifier to query your database for the required item. If your item is hosted online, you will want to make an API call to fetch the item with the desired item identifier. Once the item is obtained you can show the appropriate screen in your app.

In Summary

In this week's post, you learned how you can index your app's content in iOS' Spotlight index. I showed you how you can use user activities and searchable items to add your app's content to Spotlight. Doing this will make your app show up in many more places in the system, and can help your user discover content in your app.

If you want to learn much more about Spotlight's index, I have a full chapter dedicated to it in my book Mastering iOS 12 Development. While this book isn't updated for iOS 13 and the Scene Delegate, I think it's still a good reference to help you make sense of Spotlight and what you can do with it.

If you have any questions or feedback about this post, don't hesitate to reach out to me on Twitter.

Categories

App Development

Subscribe to my newsletter