Episode #193

In App Search

17 minutes
Published on October 15, 2015

This video is only available to subscribers. Get access to this video and 584 others.

In this episode we take a look at how to provide deep search in our iOS applications. We start by leveraging NSUserActivity to provide relevant search results for recent activities to users. Then we look at how to customize the search results with rich metadata. Finally we look at using CoreSpotlight to fully index the content to make it available for search after we launch our app.

Episode Links

Indexing User Activities

userActivity = NSUserActivity(activityType: "com.companyroster.viewcontact")
userActivity?.requiredUserInfoKeys = ["contactName", "contactDepartment"]
userActivity?.title = contact.name
userActivity?.delegate = self
userActivity?.eligibleForHandoff = false
userActivity?.eligibleForSearch = true
userActivity?.eligibleForPublicIndexing = false

userActivity?.becomeCurrent()

Then, make sure to provide the items in userInfo when it is about to be saved:

func userActivityWillSave(userActivity: NSUserActivity) {
    userActivity.addUserInfoEntriesFromDictionary([
        "contactName": contact!.name,
        "contactDepartment": contact!.department
    ])
}

In my testing, setting userInfo on the view controller's userActivity property didn't seem to have an effect unless I did this. For your own custom (strongly held) NSUserActivity properties it worked as expected. If you do this, make sure you have a strong reference to the user activity, and that you properly call resignCurrent when the view is going away. the built-in property does this for you.

Enrich Search Results with Custom Metadata

to provide more than a title and your app icon, you can provide a searchable attribute set that gives you more control over how search results are displayed.

First, we define an extension that gives us our searchable attribute set:

import CoreSpotlight
import UIKit

extension Contact {
    func searchableAttributeSet() -> CSSearchableItemAttributeSet {
        let attr = CSSearchableItemAttributeSet(itemContentType: "com.companyroster.contact")
        attr.title = name
        attr.keywords = ["nsscreencast", department]
        attr.contentDescription = "In department: \(department)"
        attr.thumbnailData = UIImageJPEGRepresentation(UIImage(named: imageName)!, 1.0)
        return attr
    }
}

Then set these attributes on the user activity:

userActivity?.contentAttributeSet = contact.searchableAttributeSet()

Indexing All Records

For content-based applications like this, it's a better idea to index all the content at once, so the user doesn't have to visit the screen to have it indexed. To do this, we'll use CoreSpotlight directly.

func indexContacts() {
    CSSearchableIndex.defaultSearchableIndex().deleteAllSearchableItemsWithCompletionHandler(nil)

    let contacts = ContactsStore().getContacts()
    let items = contacts.map { c in
        return CSSearchableItem(uniqueIdentifier: c.name,
            domainIdentifier: c.department,
            attributeSet: c.searchableAttributeSet())
    }

    CSSearchableIndex.defaultSearchableIndex().indexSearchableItems(items) { (error) in
        if error == nil {
            print("Indexed \(contacts.count) contacts.")
        } else {
            print("Error: \(error)")
        }
    }
}

Place this in your AppDelegate.swift and then your content will be searchable upon first launch.