Stuart Breckenridge

Searchable App Content with Core Spotlight

Nothing to see in search...yet!

The Core Spotlight API allows developers to add their app’s content to the on-device index on iOS devices, allowing that content to be searched and accessed directly using the search bar on the home screen. In this post, we’ll add functionality that will both add and delete content to the on-device index, in addition to restoring application state when the user accesses the app via a Core Spotlight search result.

First, let’s discuss what is stored in the on-device index.

Elements in the on-device index are CSSearchableItems. A CSSearchableItem is initialised with a uniqueIdentifier, an optional domainIdentifier, a CSSearchableItemAttributeSet, and, optionally, an expiryDate. The CSSearchableItemAttributeSet contains the properties that are displayed to the user in the search results, for example, the title and the thumbnailData.

In the example code that follows:

  • We will, at the request of the user, add each version of OS X in the OSX.history array to the on-device index.
  • Each version of OS X will correspond to a single CSSearchableItem.
  • The CSSearchableItemAttributeSet of each CSSearchableItem will contain the name of the OS, the OS icon, and the OS description.
  • We will provide a method to remove the CSSearchableItems from the on-device using their domainIdentifier.
  • When the user access the app via a Core Spotlight search, we will highlight the OS version that was selected when the app is restored to the foreground.

Adding to the On-Device Index

To keep things simple, a new UIBarButtonItem has been added to the navigation bar which will call the following method:

@IBAction func presentSpotlightOptions(sender: AnyObject)

To spare you the boiler plate, this method will present a UIAlertController to the user allowing them either Add to Core Spotlight, Remove from Core Spotlight, or dismiss the controller.

To create CSSearchableItems, their respective CSSearchableItemAttributeSets, and add them to the index, we will extend the functionality of the OSX class with a new method:

func addHistoryToCoreSpotlight(result:CoreSpotlightResult)

In this method, we initially create a temporary [CSSearchableItem] array, and then enumerate over the OSX.history array. For each dictionary entry in the OSX.history array, a CSSearchableItemAttributeSet is created and provided with the name of the OS, the description of the OS, and the icon image of the OS, for its respective title, contentDescription, an array of keywords, and thumbnailData properties.

Following the creation of the CSSearchableItemAttributeSet, we initialise a CSSearchableItem and provide it with the entry’s index as the uniqueIdentifier, a static domainIdentifier of com.osxhistory.indexedItems, and the aforementioned CSSearchableItemAttributeSet. Each CSSearchableItem is added to the temporary [CSSearchableItem] array.

The code for this is shown below:

var itemsToAdd = [CSSearchableItem]() 
for (index, os) in history.enumerate()
	let uniqueID = index 

	let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeText as String) 
	attributeSet.title = os["name"] as? String! 
	attributeSet.contentDescription = os["description"] as? String! 
	attributeSet.thumbnailData = UIImagePNGRepresentation(UIImage(named: (os["image"] as? String!)!)!) 
	attributeSet.keywords = ["OS X", (os["name"] as? String!)!, (os["version"] as? String!)!] 

	let searchableItem = CSSearchableItem(uniqueIdentifier: String(uniqueID), domainIdentifier: "com.osxhistory.indexedItems", attributeSet: attributeSet) 
	searchableItem.expirationDate = NSDate().dateByAddingTimeInterval(600) 


In the example above the CSSearchableItem has been set with an expirationDate of 10 minutes from the point it was created. This means that 10 minutes after the data is indexed, it will be automatically removed.

Once this is complete, we are ready to have the content indexed and to do that, we call the following method:

CSSearchableIndex.defaultSearchableIndex().indexSearchableItems(itemsToAdd) { (error) in
	if error != nil
			completionHandler(error: error!)
	} else{
			completionHandler(error: nil)

In the implementation above the CoreSpotlightResult1 completionHandler is called on the main thread after the CSSearchableItems have been journaled by the index. We respond to the CoreSpotlightResult by displaying a success or error message depending whether an NSError is provided by the block.

With all this work complete we can now search for our app’s content using the Spotlight search bar.


Restoring State

Without any additional code, tapping on an OS X History result will open the app and do nothing. That’s not very interesting! Let’s do something quite contrived.

In the app delegate, add the following code:

func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool {
        if userActivity.activityType == CSSearchableItemActionType {
            let uniqueId = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String
            let navigationController = window?.rootViewController as? UINavigationController
            let viewController = navigationController?.topViewController as? ViewController
        return true

This method lets the app delegate know that data is available to restore state or continue an activity. What we’re doing above is extracting the uniqueId of the CSSearchableItem (which was its index in the OSX.history array) and then passing it to a new method—restoreState(row:Int)—on the ViewController.

The restoreState(row:Int) method will scroll to indexPath of the selected search result and then, magically, spin the OS X icon.

// Create an `indexPath` and scroll to the `indexPath.row`.
let path = NSIndexPath(forRow: row, inSection: 0)
osXTableView.scrollToRowAtIndexPath(path, atScrollPosition: .Top, animated: false)

// Rotate the image view of the cell at the indexPath
let cell = osXTableView.cellForRowAtIndexPath(path) as! OSXCell
let rotation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.toValue = M_PI * 2.0
rotation.duration = 1.0
rotation.repeatCount = 1
rotation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
cell.imageView?.layer.addAnimation(rotation, forKey: "rotationAnimation")

Removing from the On-Device Index

To remove data from the index, we can tap our UIBarButtonItem again, and select Remove from Core Spotlight. This will then call the following method on the OSX class:

func removeHistoryFromCoreSpotlight(completionHandler: CoreSpotlightResult)
        CSSearchableIndex.defaultSearchableIndex().deleteSearchableItemsWithDomainIdentifiers(["com.osxhistory.indexedItems"]) { (error) in
            if error != nil
                    completionHandler(error: error!)
            } else{
                    completionHandler(error: nil)

This removes all CSSearchableItems with the domainIdentifier of com.osxhistory.indexedItems. In short, all our OS X entries will be removed from the index. Like the addHistoryToCoreSpotlight method, the CoreSpotlightResult block is called at the conclusion of the method and will pass an error if there has been a problem deleting the data.2

Wrapping Up

In this post, using the Core Spotlight API, we’ve achieved the following:

  • Adding data to the on-device index
  • Removing data from the on-device index
  • Restoring state by reading the NSUserActivity data passed to the app delegate

Updated app code is available on GitHub.

  1. typealias CoreSpotlightResult = (error:NSError?) -> () ↩︎

  2. If you wish to see errors, try running this code in the iPhone 4s simulator. ↩︎

Swift 3.0 Release Process

From the Swift Blog:

Swift 3.0 is a major release that is not source-compatible with Swift 2.2. It contains fundamental changes to the language and Swift Standard Library. A comprehensive list of implemented changes for Swift 3.0 can be found on the Swift evolution site.

Swift 3.0 is expected to be released sometime in late 2016. In addition to its release, Swift 3.0 will ship in a future version of Xcode.

Exciting times.

Default .gitignore for iOS and OS X Projects

Whenever you start a new Xcode project you are presented with the option of creating a Git repository on your Mac or on a server. The issue with this is that you don’t get a .gitignore file by default, so your initial commit is full of stuff you don’t necessarily need, like .DS_Store. There is an option to provide a default .gitignore when a new repository is initialised.

First, open/create your .gitconfig file in Terminal:

nano ~/.gitconfig

Add the following configuration details:

        excludesfile = ~/.gitignore

Save the file: Ctrl + X, Y, Enter

Then, open/create a new .gitignore at root level:

nano ~/.gitignore

Add the following configuration details to ensure that git intentionally will not track these files:

# Xcode
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## Build generated

## Various settings

## Other

## Obj-C/Swift specific

## Playgrounds

# Swift Package Manager
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/

# CocoaPods
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# Pods/

# Carthage
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts


# fastlane
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 
# screenshots whenever they are needed.
# For more information about the recommended setup visit:



Save the file: Ctrl + X, Y, Enter

Subsequent git repositories will read from this .gitignore file when being initialised.

Note: If you wish to override the patterns in this .gitignore, you can add a local (to the repo) .gitignore that negates the pattern. For example, if you wanted to include playground.xcworkspace in the commit, you would add !playground.xcworkspace to the local .gitignore file.

Recommended Reading:

  1. Git - gitignore Documentation (via Git-SCM).

Implementing UISearchController

With iOS 8, Apple introduced a more streamlined approach to incorporating a UISearchBar and associated search functionality. In this post, we look at how to implement UISearchController.

Before we look at UISearchController, we need to create a UITableView and seed it with some data. I’ll spare you the boilerplate of creating a table view, but with WWDC around the corner—where we’re going to learn about the future of OS X (or is it macOS?)—my data source is a run down of the history of OS X releases. Topical, right? The data source—history—is created in a separate class called OSX, along with a searchResults array:

class OSX {
    static let sharedOSX = OSX()
    var history:[Dictionary<String,Any>] = [
            "name" : "Cheetah",
            "version" : "10.0",
            "released" : "March 24, 2001",
            "image" : "Cheetah"
            "name" : "Puma",
            "version" : "10.1",
            "released" : "September 25, 2001",
            "image" : "Cheetah"
            "name" : "Jaguar",
            "version" : "10.2",
            "released" : "August 23, 2002",
            "image" : "Jaguar"
        // and so on...
    var searchResults = [Dictionary<String,Any>]()

When populated into the tableview it looks like this:

What do we do to add a UISearchController to the tableview? It’s really quite simple. Within your tableview’s view controller, create a lazy variable for your UISearchController and configure it as you see fit. My example implementation is below:

lazy var searchController:UISearchController = ({
        let controller = UISearchController(searchResultsController: nil) // 1
        controller.hidesNavigationBarDuringPresentation = false // 2
        controller.dimsBackgroundDuringPresentation = false // 3
        controller.searchBar.searchBarStyle = .Minimal // 4
        controller.searchResultsUpdater = self // 5
        return controller

In the order of what is going on here:

  1. If you want to present search results in the current view controller, pass nil as the parameter.
  2. I want to keep the UINavigationBar visible when using the search bar, so this property is set to false.
  3. If you want the background to be dimmed during a search, use true, for this implementation, I’m keeping it as false.
  4. It’s personal preference which search bar style you use. I’m using .Minimal.
  5. In order for a UISearchController to work, you have to assign an object that conforms to the UISearchResultsUpdating protocol. For this sample code, I’m assigning it to self, which is the view controller.

To make the searchController’s searchBar visible, you add a one-liner in viewDidLoad() setting the searchBar as the tableHeaderView:

osXTableView.tableHeaderView = searchController.searchBar

We then need to ensure that our view controller conforms to the UISearchResultsUpdating protocol:

extension ViewController:UISearchResultsUpdating
    func updateSearchResultsForSearchController(searchController: UISearchController) {
        OSX.sharedOSX.searchResults = OSX.sharedOSX.history.filter({
            ($0["name"] as! String).lowercaseString.containsString(searchController.searchBar.text!.lowercaseString) ||
            ($0["version"] as! String).lowercaseString.containsString(searchController.searchBar.text!.lowercaseString) ||
            ($0["released"] as! String).lowercaseString.containsString(searchController.searchBar.text!.lowercaseString)

In this method we are updating the searchResults array by filtering the history array based on the text in the searchController’s searchBar1. Once the array has been filtered, the tableView is reloaded.

Finally, in order to ensure that the searchResults are displayed correctly, we have to make two small amendments to numberOfRowsInSection and cellForRowAtIndexPath to accomodate for the searchController being active:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        switch {
        case true:
            return OSX.sharedOSX.searchResults.count
        case false:
            return OSX.sharedOSX.history.count
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
	let cell = tableView.dequeueReusableCellWithIdentifier("OSXCell") as! OSXCell
	switch {
	case true:
	case false:
	return cell

The only difference between configureSearchCell and configureCell is the array from which each method retrieves data.

Once all the code is in place, we now have a working search bar!

The source code for this example is available on Github.

Recommended Reading:

  1. UISearchController Class Reference (via Apple)
  2. UISearchResultsUpdating Protocol Reference (via Apple)
  1. The strings contained within the data source and the search bar are lowercased so that, for example, “Ee” in the search bar will correctly match the “ee” from Cheetah. ↩︎

Performance Analysis of appendIfUnique

Last week we looked at extending Array to include an appendIfUnique function. In this post—inspired by feedback from Twitter—we assess the performance of that function against the built in contains(element: Self.Generator.Element) and Swift’s Set type.

The difference between O(n2) and O(n) can be seen in this graph:


To see how close we are to O(n2) I’ve created three test cases:

  • one will insert n elements to a Set and then create an Array from that set;
  • one will append n unique elements to an Array using appendIfUnique; and,
  • one will append n unique elements to an Array using the contains(element:Self.Generator.Element) function
struct Benchmarks { 
    static func testNumbersSetToArray(size:Int) {
        var set = Set<Int>()
        while set.count < size {
            set.insert(set.count + 1)
        let _ = Array(set)
    static func testAppendIfUnique(size:Int) {
        var numbers = [Int]()
        while numbers.count < size {
            numbers.appendIfUnique(numbers.count + 1)
    static func testArrayContains(size:Int) {
        var numbers = [Int]()
        while numbers.count < size {
            if !numbers.contains(numbers.count + 1)
                numbers.append(numbers.count + 1)

Function performance is measured from XCTestCase by calling:

    func testAppend() {
        self.measureBlock {
    func testArrayContains(){
        self.measureBlock {
    func testSet(){
        self.measureBlock {

I ran tests with n going up to 60,000 and the compiler optimisation level set to Fast[-0] Whole Module Optimization. The results are below:

Elements appendIfUnique contains Set
10000 0.015s 0.029s 0.002s
20000 0.064s 0.112s 0.003s
30000 0.141s 0.251s 0.004s
40000 0.243s 0.450s 0.004s
50000 0.421s 0.695s 0.006s
60000 0.607s 1.019s 0.007s
Performance Results

Conclusion: Using Set is an outright winner, while appendIfUnique and contains are in line with O(n2). contains is, however, considerably slower than appendIfUnique which is surprising.

The source code for these tests is available on GitHub.

Recommended Reading: