Handoff Tutorial: Getting Started
Learn how to use the new Handoff API introduced in iOS 8 to allow users to continue their activities across different devices. By Soheil Azarpour.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Handoff Tutorial: Getting Started
35 mins
- Handoff Overview
- Getting Started
- Device Compatibility: iOS
- User Activities
- Activity Types
- The Starter Project
- Setting Your Team
- Configuring Activity Types
- Quick End-to-End Test
- Creating the View Activity
- Finishing Touches
- Creating the Edit Activity
- Finishing Touches
- Receiving the Activities
- Finishing Touches
- Versioning Support
- Handoff Best Practices
- Where To Go From Here?
Receiving the Activities
When the user launches your app through Handoff, the app delegate does most of the processing of the incoming NSUserActivity
.
Assuming that everything goes well and the data transfers successfully, iOS then calls application(_:continueUserActivity:restorationHandler:)
. This is your first chance to interact with the NSUserActivity
instance.
You already have an implementation in place from previous sections. Update it as follows:
func application(application: UIApplication,
continueUserActivity userActivity: NSUserActivity,
restorationHandler: (([AnyObject]!) -> Void))
-> Bool {
if let window = self.window {
window.rootViewController?.restoreUserActivityState(userActivity)
}
return true
}
You pass the userActivity
to the rootViewController
of the app’s window and return true. This tells the OS you handled the Handoff action successfully. From this point on, you are on your own to forward calls and restore the activity.
The method you call on the rootViewController
is restoreUserActivityState (activity:)
. This is a standard mehod that is declared at UIResponder
level. The OS uses this method to tell a receiver to restore an instance of NSUserActivivty
. It is OK for you to call this method and pass on the userActivity
.
Your task now is to walk down the view controller hierarchy and pass the activity from the parent to child view controllers until reach the point where the activity is consumed:
The root view controller is a TraitOverrideViewController
, and its job is to manage the size classes of the application; it won’t be interested in your user activity.
Open TraitOverrideViewController.swift and add the following:
override func restoreUserActivityState(activity: NSUserActivity) {
let nextViewController = childViewControllers.first as! UIViewController
nextViewController.restoreUserActivityState(activity)
super.restoreUserActivityState(activity)
}
Here you grab the first child view controller contained by the TraitOverrideViewController
and pass the activity down to it. It’s safe to do this, since you know your app’s view controller will only contain one child.
The next view controller in the hierarchy is a SplitViewController
, where things get a little more interesting.
Open SplitViewController.swift and add the following:
override func restoreUserActivityState(activity: NSUserActivity) {
// What type of activity is it?
let activityType = activity.activityType
// This is an activity for ListViewController.
if activityType == ActivityTypeView {
let controller = viewControllerForViewing()
controller.restoreUserActivityState(activity)
} else if activityType == ActivityTypeEdit {
// This is an activity for DetailViewController.
let controller = viewControllerForEditing()
controller.restoreUserActivityState(activity)
}
super.restoreUserActivityState(activity)
}
SplitViewController
knows about both ListViewController
and DetailViewController
. If the NSUserActivity
is a List Viewing activity type, you’ll pass it to ListViewController
. However, if it’s an Editing activity type you’ll pass it to DetailViewController.
You’ve passed the activities to all the correct places – now it’s time to get some data from those activities.
Open ListViewController.swift and implement restoreUserActivityState(activity:)
as follows:
override func restoreUserActivityState(activity: NSUserActivity) {
// Get the list of items.
if let userInfo = activity.userInfo {
if let importedItems = userInfo[ActivityItemsKey] as? NSArray {
// Merge it with what we have locally and update UI.
for anItem in importedItems {
addItemToItemsIfUnique(anItem as! String)
}
PersistentStore.defaultStore().updateStoreWithItems(items)
PersistentStore.defaultStore().commit()
tableView.reloadData()
}
}
super.restoreUserActivityState(activity)
}
In the above method you finally get to continue a viewing activity. Since you want to maintain a unique list of shopping items, you only add those items that are unique to your local list, then save and update the UI once you’re done.
Build and run. At this point you should be able to see the list of items that are received from another device via Handoff.
Editing activities are handled in a very similar manner. Open DetailViewController.swift and implement restoreUserActivityState(activity:)
as follows:
override func restoreUserActivityState(activity: NSUserActivity) {
if let userInfo = activity.userInfo {
var activityItem: AnyObject? = userInfo[ActivityItemKey]
if let itemToRestore = activityItem as? String {
item = itemToRestore
textField?.text = item
}
}
super.restoreUserActivityState(activity)
}
This retrieves the information about the edit activity and updates the text field appropriately.
Build and run again to see it in action!
Finishing Touches
When the user indicates that they want to continue a user activity on another device by swiping up on the app icon, the OS launches the corresponding app. Once the app is launched, the OS calls on application(_, willContinueUserActivityWithType:)
. Open AppDelegate.swift and add the following method:
func application(application: UIApplication,
willContinueUserActivityWithType userActivityType: String)
-> Bool {
return true
}
At this point your app hasn’t yet downloaded the NSUserActivity
instance and its userInfo
payload. For now, you’ll simply return true
. This forces the app to accept the activity each time the user initiates the Handoff process. If you want to alert your user that the activity is on its way, this is the place to do it.
At this point the OS has started transferring data from one device to another. You have already covered the case where everything goes well. But it is conceivable that the Handoff activity will fail at some point.
Add the following method to AppDelegate.swift to handle this case:
func application(application: UIApplication,
didFailToContinueUserActivityWithType userActivityType: String,
error: NSError) {
if error.code != NSUserCancelledError {
let message = "The connection to your other device may have been interrupted. Please try again. \(error.localizedDescription)"
let alertView = UIAlertView(title: "Handoff Error", message: message, delegate: nil, cancelButtonTitle: "Dismiss")
alertView.show()
}
}
If you receive anything except NSUserCancelledError
, then something went wrong along the way and you won’t be able to restore the activity. In this case, you display an appropriate message to the user. However, if the user explicitly canceled the Handoff action, then there’s nothing else for you to do here but abort the operation.
Versioning Support
One of the best practices when working with Handoff is versioning. One strategy to deal with this is to add a version number to each handoff that you send, and only accept handoffs from your version number (or potentially earlier). Let’s try this.
Open Constants.swift and add the following constants:
let ActivityVersionKey = "shopsnap.version.key"
let ActivityVersionValue = "1.0"
The above version key and value are arbitrary key-value you picked for this version of the app.
If you recall from the previous section, the OS periodically and automatically calls restoreUserActivityState(activity:)
. The implementations of this method were very focused and were limited to the scope of the object that implemented it. For example, ListViewController
overrode this method to update userActivity
with list of all items, whereas DetailViewController
overrode to update with the current item that was being edited.
When it comes to something that is generic to your userActivity
and applies to all of your user activities regardless, like versioning, the best place to do that is in the AppDelegate.
Whenever restoreUserActivityState(activity:)
is called, the OS calls application(application:, didUpdateUserActivity userActivity:)
in the app delegate right after that. You’ll use this method to add versioning support to your Handoff.
Open AppDelegate.swift and add the following:
func application(application: UIApplication,
didUpdateUserActivity userActivity: NSUserActivity) {
userActivity.addUserInfoEntriesFromDictionary([ActivityVersionKey: ActivityVersionValue])
}
Here you simply update the userInfo dictionary with the version of your app.
Still in AppDelegate.swift, update the implementation of application(_:, continueUserActivity: restorationHandler:)
as follows:
func application(application: UIApplication,
continueUserActivity userActivity: NSUserActivity,
restorationHandler: (([AnyObject]!) -> Void))
-> Bool {
if let userInfo: NSDictionary = userActivity.userInfo {
if let version = userInfo[ActivityVersionKey] as? String {
// Pass it on.
if let window = self.window {
window.rootViewController?.restoreUserActivityState(userActivity)
}
return true
}
}
return false
}
Here you do a sanity check on the version of the userActivity
and pass it on only if it matches the version you know about. Build and run your app once again to ensure the app runs as usual.