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.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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.

App Screenshots

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:

Setting 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:

Configuring activity types

Enter NSUserActivityTypes for the key name and make it an Array type, as shown below:

Enter NSUserActivityTypes

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.

Add two items under NSUserActivityTypes

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:

  1. startUserActivity() is a helper function that creates an instance of NSUserActivity with a hardcoded shopping list. Then it starts broadcasting that activity by calling becomeCurrent().
  2. After you call becomeCurrent(), OS will periodically call updateUserActivityState(). UIViewController inherits this method from UIResponder, and you should override this to update the state of your userActivity here.
    Here you update the shopping list with the same hardcoded values as before, since this is just a test. Note that addUserInfoEntriesFromDictionary is the preferred way of mutating userInfo dictionary of NSUserActivity. You should always call super.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.

    1. Install and run the app on your first device.
    2. 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.
  1. Install and run the app on your first device.
  2. 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.

Console Log

  1. 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.

Lock screen

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!