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?
The Starter Project
Start by downloading the starter project for this Handoff tutorial. Once you’ve downloaded it, open up the project in Xcode and run it in an iPhone simulator.
It is called ShopSnap. You can build a simple shopping list in this app. A shopping item is represented by a String and you store the shopping list as an Array of strings. Tapping the + button adds a new item to the list and swiping removes an item.
You’ll define two distinct user activities for this app:
- Viewing the list. If the user is currently viewing the list, you’ll Handoff the entire array of items.
- Adding or editing an item. If the user is currently adding a new item, you’ll Handoff an “edit” activity for a single item instead.
Setting Your Team
For Handoff to work, both the sending and receiving app must be signed by the same team. Since this app is both the sender and the receiver, this is simple!
Select your ShopSnap project, and in the general tab, switch the Team to your team:
Build and run on one of your Handoff-compatible iOS devices to make sure it runs OK, then continue on.
Configuring Activity Types
The next step is to configure the activity types your app supports. Open Supporting Files\Info.plist and click on the + button that appears next to Information Property List to add a new item under the Information Property List dictionary:
Enter NSUserActivityTypes
for the key name and make it an Array
type, as shown below:
Add two items under NSUserActivityTypes
(Item 0 and Item 1) and set their types to String. Enter com.razeware.shopsnap.view for Item 0, and com.razeware.shopsnap.edit for Item 1.
These are arbitrary activity types specific and unique to your app. Since you’ll refer to them from multiple places in the app, it’s good practice to add them as constants in a separate file.
Right-click on the ShopSnap group in the project navigator, select New File \ iOS \ Source \ Swift File. Name the class Constants.swift and ensure your new class is added to the ShopSnap target.
Add the following code to your class:
let ActivityTypeView = "com.razeware.shopsnap.view"
let ActivityTypeEdit = "com.razeware.shopsnap.edit"
let ActivityItemsKey = "shopsnap.items.key"
let ActivityItemKey = "shopsnap.item.key"
Now you can use these constants for the two different activity types. You’ve also defined some constants for the keys you’ll be using in the user activity’s userInfo
dictionary for convenience.
Quick End-to-End Test
Let’s run a quick end-to-end test to make sure that your devices can communicate properly.
Open ListViewController.swift and add the following two functions:
// 1.
func startUserActivity() {
let activity = NSUserActivity(activityType: ActivityTypeView)
activity.title = "Viewing Shopping List"
activity.userInfo = [ActivityItemsKey: ["Ice cream", "Apple", "Nuts"]]
userActivity = activity
userActivity?.becomeCurrent()
}
// 2.
override func updateUserActivityState(activity: NSUserActivity) {
activity.addUserInfoEntriesFromDictionary([ActivityItemsKey: ["Ice cream", "Apple", "Nuts"]])
super.updateUserActivityState(activity)
}
This is a quick test where you hard code a user activity, to make sure that you can receive it OK on the other end.
Here is what the code above does:
-
startUserActivity()
is a helper function that creates an instance ofNSUserActivity
with a hardcoded shopping list. Then it starts broadcasting that activity by callingbecomeCurrent()
. - After you call
becomeCurrent()
, OS will periodically callupdateUserActivityState()
.UIViewController
inherits this method fromUIResponder
, and you should override this to update the state of youruserActivity
here.
Here you update the shopping list with the same hardcoded values as before, since this is just a test. Note thataddUserInfoEntriesFromDictionary
is the preferred way of mutatinguserInfo
dictionary ofNSUserActivity
. You should always callsuper.updateUserActivityState()
at the end.
Now you just need to start this method. Add the following line to the beginning of viewDidLoad()
:
startUserActivity()
That’s the minimum you need to start broadcasting – let’s move on to receiving. Open AppDelegate.swift and add the following code:
func application(application: UIApplication,
continueUserActivity userActivity: NSUserActivity,
restorationHandler: (([AnyObject]!) -> Void))
-> Bool {
let userInfo = userActivity.userInfo as NSDictionary
println("Received a payload via handoff: \(userInfo)")
return true
}
This method on AppDelegate is called when everything goes well and a userActivity
is successfully transferred. Here you log a message with the userInfo
dictionary of the userActivity
. You return true
to indicate you handled the user activity.
Let’s try this out! There’s a little bit of coordination required to get this working on two devices, so follow along carefully.
- Install and run the app on your first device.
- Install and run the app on your second device. Make sure that you are debugging the app in Xcode so you can see your
println()
output.
- Install and run the app on your first device.
- Install and run the app on your second device. Make sure that you are debugging the app in Xcode so you can see your
println()
output.
Note: It is also useful to have the console logs open in this step so you can see any output from iOS, in case there is an issue with the connection. To see the console logs, go to Window\Devices, select your device, and select the icon in the lower left to expand the console area.
- Put the second device to sleep by pressing the power button. On the same device, press the Home button. If everything works fine, you should see the ShopSnap app icon appear at the left bottom corner of the screen. From there you should be able to launch the app, and see the log message in Xcode’s console:
Received a payload via handoff: {
"shopsnap.items.key" = (
"Ice cream",
Apple,
Nuts
);
}
If you don’t see the app icon on the lock screen, close and re-open the app on the originating device. This forces the OS to restart broadcasting again. Also check the device console to see if there are any error messages from Handoff.
Creating the View Activity
Now that you have a basic working Handoff app, it is time to extend it. Open ListViewController.swift and update startUserActivity()
by passing the actual array of items instead of hardcoded values. Update the method to the following:
func startUserActivity() {
let activity = NSUserActivity(activityType: ActivityTypeView)
activity.title = "Viewing Shopping List"
activity.userInfo = [ActivityItemsKey: items]
userActivity = activity
userActivity?.becomeCurrent()
}
Similarly, update updateUserActivityState(activity:)
in ListViewController.swift to pass the array of items instead of hardcoded values:
override func updateUserActivityState(activity: NSUserActivity) {
activity.addUserInfoEntriesFromDictionary([ActivityItemsKey: items])
super.updateUserActivityState(activity)
}
Note: Whenever updateUserActivityState(activity:)
is called, the userInfo
dictionary is usually empty. You don’t have to empty the dictionary, just update it with appropriate values.
Now, update viewDidLoad()
in ListViewController.swift to start the userActivity
after successfully retrieving items from previous session (and only if it’s not empty), as follows:
override func viewDidLoad() {
title = "Shopping List"
weak var weakSelf = self
PersistentStore.defaultStore().fetchItems({ (items:[String]) in
if let unwrapped = weakSelf {
unwrapped.items = items
unwrapped.tableView.reloadData()
if items.isEmpty == false {
unwrapped.startUserActivity()
}
}
})
super.viewDidLoad()
}
Of course, if the app starts with an empty list of items, now the app will never start broadcasting the user activity. You need to fix this by starting the user activity once the user adds an item to the list for the first time.
To do this, update the implementation of the delegate callback detailViewController(controller:didFinishWithUpdatedItem:)
in ListViewController.swift as follows:
func detailViewController(#controller: DetailViewController,
didFinishWithUpdatedItem item: String) {
// ... some code
if !items.isEmpty {
startUserActivity()
}
}
There are three possibilities here:
- The user has updated an existing item.
- The user has deleted an existing item.
- The user has added a new item.
The existing code handles all possibilities; you only need to add the check to start an activity if there is a non-empty list of items.
Build and run on both devices again. At this point you should be able to add a new item on one device and then hand it over to the other device!