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?
Finishing Touches
When user starts adding a new item or editing an existing item, the user is not technically viewing the list of items. So you want to stop broadcasting current activity. Similarly, there is no reason to continue broadcasting it all the items in the list are deleted. Add the following helper method in ListViewController.swift:
func stopUserActivity() {
userActivity?.invalidate()
}
In stopUserActivity()
, you invalidate the existing NSUserActivity
. This makes Handoff stop broadcasting.
With stopUserActivity()
in place, it is time to call it from appropriate places.
Update implementation of prepareForSegue(segue:, sender:)
in ListViewController.swift and as follows:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
// ... some code
stopUserActivity()
}
When the user selects a row or taps the Add button, ListViewController
prepares to segue to detail view. You invalidate the current list-viewing activity.
Still in the same file, update the implementation of tableView(_:commitEditingStyle:forRowAtIndexPath:)
as follows:
override func tableView(tableView: UITableView,
commitEditingStyle editingStyle: UITableViewCellEditingStyle,
forRowAtIndexPath indexPath: NSIndexPath) {
// ... some code
if items.isEmpty {
stopUserActivity()
} else {
userActivity?.needsSave = true
}
}
When the user deletes an item from the list you need to update the user activity accordingly. If all the items have been removed from the list, you stop broadcasting. Otherwise you set needsSave
to true
on the userActivity
. When you do that, the OS immediately calls back on updateUserActivityState(activity:)
, where you update userActivity
.
To wrap up this section, there is a situation where the user has just returned from DetailViewController
by tapping the Cancel button. This triggers an exit segue. You need to re-start the userActivity
. Update the implementation of unwindDetailViewController(unwindSegue:)
as follows:
@IBAction func unwindDetailViewController(unwindSegue: UIStoryboardSegue) {
// ... some code
startUserActivity()
}
Build and run and verify that everything works fine so far. Try adding a few items to the list and verify they pass between devices.
Creating the Edit Activity
Now you need to take care of DetailViewController
in a similar fashion. This time, however, you’ll broadcast a different activity type.
Open DetailViewController.swift and modify textFieldDidBeginEditing(textField:)
as follows:
func textFieldDidBeginEditing(textField: UITextField!) {
// Broadcast what we have, if there is anything!
let activity = NSUserActivity(activityType: ActivityTypeEdit)
activity.title = "Editing Shopping List Item"
let activityItem = (count(textField.text!) > 0) ? textField.text : ""
activity.userInfo = [ActivityItemKey: activityItem]
userActivity = activity
userActivity?.becomeCurrent()
}
The above method creates an “Editing” activity with the current contents of the item’s string.
As user continues editing the item you need to update the user activity accordingly. Still in DetailViewController.swift, update the implementation of textFieldTextDidChange(notification:)
as shown below:
func textFieldTextDidChange(notification: NSNotification) {
if let text = textField!.text {
item = text
}
userActivity?.needsSave = true
}
Now that you have indicated the activity needs to be updated, implement updateUserActivityState(activity:)
to update it whenever the OS asks for it:
override func updateUserActivityState(activity: NSUserActivity) {
let activityListItem = (count(textField!.text!) > 0) ? textField!.text : ""
activity.addUserInfoEntriesFromDictionary([ActivityItemKey: activityListItem])
super.updateUserActivityState(activity)
}
Here you simply update the current item to the text in the text field.
Build and run. At this point if you start adding a new item or editing an existing item on one device, you can hand over the edit process to another device.
Finishing Touches
Since needsSave
is a lightweight operation, in the code above you can set it as often as you like and continuously update userInfo
with each keypress.
There is one small design detail you may have picked up on. The view controllers are laid out as a split view on the iPad and in landscape mode on the iPhone. It’s possible to switch between items in the list without resigning the keyboard. If that happens, textFieldDidBeginEditing(textField:)
won’t be called, resulting in your user activity never being updated to the new text.
To fix this, update item’s didSet
observer in DetailViewController.swift as shown below:
var item: String? {
didSet {
if let textField = self.textField {
textField.text = item
}
if let activity = userActivity {
activity.needsSave = true
}
}
}
The DetailViewController
’s item property is set when the user taps an item in the ListViewController
. A simple fix for this situation is to let the view controller know that it has to update the activity when the item changes.
Finally, you’ll need to invalidate userActivity
when the user leaves the DetailViewController
so the edit activity is no longer broadcasted.
Simply add this line to the beginning of textFieldShouldReturn(_:)
in DetailViewController.swift:
userActivity?.invalidate()
Build and run your project to make sure the app still works as usual.
Next, you will handle the incoming activity.
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!