Blog

We believe there is something unique at every business that will ignite the fuse of innovation.

Introduction

Quick, Easy, and Available

With iOS 8 Apple introduced Handoff with NSUserActivity. Handoff allows a user to continue an activity across devices. With iOS 9 Apple takes this a step further by adding new methods and properties to NSUserActivity and creating the new CSSearchableItem class for the Core Spotlight framework. These additions allow developers to create activities or descriptive items and store them privately to an on-device or public Cloud Index. The indexed items are then available using Core Spotlight and Safari searches for quick access in your app. NSUserActivity and CSSearchableItem can also be linked to actions on a website. To improve app discoverability Apple added Web Markup to link similar website and app activities. This tutorial will demonstrate how add these new API features to you apps.

Protecting Your Data

Throughout the WWDC videos and the iOS 9 documentation Apple reiterates the importance of privacy with Search API. Private indexes are secured to an individual device. The one-device index is not sent to Apple or shared between devices. Handoff ready apps can still handoff an activity to another device but only from within the app. Public indexes are sent to Apple, checked for personal information, and stored securely on their Cloud Index. The Cloud Index though does not return every activity that every app has indexed. To reduce the clutter Apple developed a ranking methodology to decide what activities are relevant and worth displaying to users.

How to Search

NSUserActivity

Along with Handoff NSUserActivity can now index previous activities a user performs in your app, make activities available for public searching, and provide metadata to provide rich information for your users. An important thing to remember about NSUserActivity is that any properties of this type must be Strong properties in order to be indexed. This may be a beta glitch at this point or by design. The documentation does not mention this but if an item is weak it will be deallocated before it is indexed and will not appear in user searches. Another potential feature or glitch is that NSUserActivity is limited to one activity per navigation point. Only the last activity to become current will be indexed. Because of this the information attached to the activity will contain more general and less personal information than can be for a CSSearchableItem. Creating and setting up an activity can be done with as little or as much data as you’d like. To create a simple basic activity takes only a few lines of code:

var activity:NSUserActivity?
activity = NSUserActivity(activityType: "com.joshhuerkamp.bills")
activity?.title = "Pay Bills"
activity?.userInfo = ["index":"com.joshhuerkamp.bills"]
activity?.eligibleForSearch = true

This initializes the activity, sets its title, userInfo, and flags it to be indexed for search. A similar Boolean named eligibleForPublicIndexing can be set to true and will index the activity publicly on the Cloud Index. In the above example searching Core Spotlight for “Pay Bills” will return this activity.

Other options are available to increase search exposure like adding additional keywords, thumbnail images, and more specific data to your activity. Once the information has been added the activity can be indexed by calling becomeCurrent(). Below I’ll add additional search keywords and index the activity

activity?.keywords = NSSet(array: ["Bills","Pay Bills"]) as! Set
activity?.becomeCurrent()

Search Rankings

For public searching items are ranked automatically by Apple’s methodology. The methodology for ranking searches is obvious and easy to develop for. Basically the more popular an item the higher it will be ranked. The documentation for Search API lists these suggestions:

  • The frequency with which users view your content (this is only captured using NSUserActivity)
  • The amount of engagement users have with your content. That is the number of times an item is tapped by users.
  • For web markup content the popularity of a URL and the total amount of structured data available.

All items that are publicly indexed are eligible for ranking but to protect privacy and prevent duplicates not all activities will appear in the Cloud Index until they reach a ranking threshold determined by Apple. As of now Apple has not made the minimum amount of user activity is needed to meet the threshold. Activities are verified for privacy using a Zero Knowledge proof method. When a user engages with an activity a one-way hash is sent to the Cloud Index. When enough of the same hash have been sent to the Cloud Index the activity is made public and searchable.

Website to App with a Tap

With search in iOS 9 Apple tries to bridge the gap a little between websites and their app. Using a combination of:

CSSearchableItem and CSSearchableItemAttributeSet

CSSearchableItem is similar to NSUserActivity but with some important differences. First CSSearchableItem can only be used by the on-device private index. Second there is no limit to the number of items that you can create at a navigation point. CSSearchableItem can also be individually updated or removed independently of other items or in relation to similar items. This can be done as the user moves through the app or without even running the app using batch features provided by CSSearchableIndex in the Core Spotlight API.

Searchable Items can contain basic information like a title, search keywords, subject, and a display name. You can also add additional information like thumbnails and arrays containing more detail. Finally a unique String must be provided as the item’s identifier. The code below loops through upcoming bills and creates a CSSearchableItemAttributeSet which is saved to a CSSearchableItem and then indexed to the device’s private CSSearchableIndex. You may note that nowhere are the items marked as eligible for search. This is not needed for searchable items. Instead adding them to the CSSearchableIndex makes them searchable.

for row in objects { 
	// CSSearchableItemAttributes for bill data
	let searchableItem = CSSearchableItemAttributeSet(itemContentType: kUTTypeImage as String)
 
	// Set searchable keywords
	searchableItem.keywords = ["Bill","Pay Bill","Bills Due"]
 
	// Set theme, subject, content description, and name displayed of indexed item
	searchableItem.title = "Upcoming bills"
	searchableItem.subject = row["vehicleMake"].stringValue
	searchableItem.contentDescription = "Upcoming Bill"
	searchableItem.displayName = row["coBorrowerName"].stringValue
	searchableItem.identifier = "bill \(String(i))"
 
	// Create CSSearchableItem with defined attributes and index with its domain and unique identifier
	let item = CSSearchableItem(uniqueIdentifier: "com.joshhuerkamp.bills.\(i)", domainIdentifier: "com.joshhuerkamp.bills", attributeSet: searchableItem)
 
	CSSearchableIndex.defaultSearchableIndex().indexSearchableItems([item], completionHandler: { (ErrorType) -> Void in
		if (ErrorType != nil) {
			print("indexing failed \(ErrorType)")
		}
	})
	i++
}

CSSearchableItem instances can be removed individually or as a group once they are no longer needed. Items can be removed using the unique identifier, or by domain with the domain identifier, or all items can be removed from an index. For this demo when a user turns off indexing through the settengs menu all current items under the domain specified are removed:

// Delete activities from CoreSpotlight index of domain selected
func removeIndexedItems(itemsIndex:Int) {
	var domainIdentifier = ""
 
	//Determine domain to delete
	switch itemsIndex {
	case 0: domainIdentifier = "com.joshhuerkamp.transactions"
	case 1: domainIdentifier = "com.joshhuerkamp.bills"
	case 2: domainIdentifier = "com.joshhuerkamp.atm"
	default: break
	}
 
	// Delete activities
	CSSearchableIndex.defaultSearchableIndex().deleteSearchableItemsWithDomainIdentifiers([domainIdentifier]) { (ErrorType) -> Void in
		if (ErrorType != nil) {
			print("indexing failed \(ErrorType)")
		}
	}
}

Core Spotlight provides advanced features that allow batch updates while your app isn’t running. This is considered an advanced feature and is not covered in the demo here but I will list the steps needed to add it to your app. First you must add an app extension, set the indexDelegate property of CSSearchableIndex , and implement the required methods of CSSearchableIndexDelegate. Within the delegate methods you can then update all or specific indexed items. There are two require delegate methods:

searchableIndex:reindexAllSearchableItemsWithAcknowledgementHandler:







searchableIndex:reindexSearchableItemsWithIdentifiers:acknowledgementHandler:

These delegate methods can also be implemented in a custom extension extending CSIndexExtensionRequestHandler to re-index items when the index is lost or your app crashes during indexing. For background indexing the structure of the code below can be used in the background methods of you AppDelegate class:

let index:CSSearchableIndex = CSSearchableIndex()
index.beginIndexBatch()
index.indexSearchableitems:items completionHandler:nil index.deleteSearchableItemsWithIdentifiers(identifiers, completionHandler:nil)
index.endIndexBatchWithClientState(clientState completionHandler:nil)

Linking Items and Activities

CSSearchableItem , NSUserActivity, and Web Markupare designed to be linked together giving users a richer experience. Apple encourages developers to make use of this ability. The catch is that a lot of information can be duplicated between the APIs and can create duplicate search results. Along with a bad user experience this can also lower your item rankings by distributing the clicks among the duplicates. Apple suggests using the strategies below to remove duplicates:

  • If you’re using both NSUserActivity and Core Spotlight APIs to index an item, use the same value for relatedUniqueIdentifier and uniqueIdentifier to link the representations of the item.
  • If you’re using both NSUserActivity and web markup to index an item, set the user activity object’s webpageURL property to the relevant URL on your website.
  • If you’re using all three APIs, it works well to use the URL of the relevant webpage as the value for uniqueIdentifierrelatedUniqueIdentifier, and webpageURL.

Following these strategies will streamline search results and improve item rankings.

Restoring Activities

Both NSUserActivity and CSSearchableItem use the same delegate method to restore your apps activities and items. The same delegate method for Handoff is used for the Search API application:continueUserActivity:restorationHandler. If your app is compatible with Handoff then you’ve already done most of the work. It is up to you to route the user to the proper area they want to see in your app. To enhance the user’s experience Apple recommends displaying the information as smoothly and quickly as possible. Try to minimize load times by using Search API data and do not present any confirmation or popup screens delaying access to the content the user wants to see. The more information you index the easier this will be. You will see with this delegate only a NSUserActivity is provided. Core Spotlight only returns an activity to your app whether it was indexed as an activity or an item. Data will be contained within the activity object in the contentAttributSet property. Unfortunately in the beta builds I have not been able to successfully populate the contentAttributeSet. For this demo the delegate code selecting an activity will return the user to that activity. The example below unpacks the provided activity and passes it to the startup view controller to handle routing the user.

// Continue NSUser activity chosen by the user from CoreSpotlight
func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool {
	let splitViewController = self.window!.rootViewController as! UISplitViewController
	let navController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as! UINavigationController
 
	// Return app state to the root view controller
	navController.popToRootViewControllerAnimated(false)
 
	// Dismiss any modals that may have been previously presented
	if let modal = navController.presentedViewController {
		modal.dismissViewControllerAnimated(false, completion: nil)
	}
 
	// Display chosen user activity and it's data
	let vc = navController.topViewController as! ChooseActivityViewController
	vc.presentActivityView(userActivity)
 
	return true
}

Automatic Expiration

NSUserActivity and CSSearchableItem both have an expirationDate property that determines when to automatically expire and remove and item from the index. The default limit for both is one month but can be set to be longer or shorter. CSSearchableItemAttributeSet also has NSDate properties that provide even more relevant info to the user. These include dueDate, completionDate, startDate, endDate, and importantDates. These are also used by Core Spotlight to create Siri Reminders. Unfortunately I was not able to get Reminders to work correctly with the current beta. Hopefully I will be able to add an example once iOS 9 is released.

Issues in Beta

If you’re reading this you probably have some experience with the many bugs that appear in Apple’s beta process and know how frustrating they can be. My experiences with this demo are no different. Even now in Beta 4 less than a month from the release of iOS 9 there are still many issues with Search API. I’ve mentioned some of the issues I’ve run into in this tutorial, but I want to list them here all in one place. As you work with Search API you can use this as a sanity check.

  • Finding indexed content – This has been steadily improving throughout the Beta releases, but there are still times where an indexed activity will not display in the search results.
  • Items to activities Core Spotlight conversion – Core Spotlight only displays activities whether they were indexed as an item or an activity. Data saved in one property as an item will be saved under a different property after being converted to an activity.
  • Continuing activities – Because of the conversion issues it can take a few attempts to correctly access information in the restorationHandler.
  • Core Spotlight unstable in iOS 9 – We all know Beta builds can be unstable and Search API and Core Spotlight are no different. Trying to access Core Spotlight after installing an app with Search API or after indexing items or activities can cause the OS to crash and restart.

Conclusions

Core Spotlight can be a very powerful tool. It can help drive web traffic to your app and increase usability for the most used features of your app. Once the GM version of iOS 9 is released and the remaining issues are resolved Search API will become widespread among apps, the web and users.

Attachments