Handoff Tutorial: Getting Started

Soheil Azarpour
Learn how to add handoff to your iOS 8 apps!

Learn how to add handoff to your iOS 8 apps!

Update 04/23/2015: Updated for Xcode 6.3 and Swift 1.2.

Note from Ray: This is an abbreviated version of a chapter from iOS 8 by Tutorials released as part of the iOS 8 Feast to give you a sneak peek of what’s inside the book. We hope you enjoy!

Handoff is a new feature in iOS 8 and OS X Yosemite. Handoff lets you continue an activity uninterrupted when you switch from one device to another – without the need to reconfigure either device.

You can add handoff feature to your iOS and OS X apps in iOS 8 and Yosemite. In this tutorial you’ll learn the basic workings of Handoff and how to use it on iOS with a non-document-based app.

Handoff Overview

Before you dive into the code, it’s important to get a high level overview of some important Handoff concepts:

  • Getting Started: Learn if your devices are Handoff-compatible and run a basic test to make sure things are working.
  • User Activities: Learn about the core unit of work with Handoff: user activities.
  • Activity Types: Learn how you can set an “activity type” on a user activity and what that means.

Getting Started

Not only can Handoff transfer your current activity from an iOS to an OS X device, but it can also transfer your current activity between iOS devices.

At the time of writing this tutorial, Handoff does not work on the iOS simulator. Therefore, to follow along with this tutorial, you will need two Handoff-compatible iOS devices.

Device Compatibility: iOS

To check whether your iOS device is Handoff compatible, go to Settings and select General from the list. If you see Handoff & Suggested Apps in the list, your device is Handoff compatible. The following screenshots show General settings menu of an iPhone 5s (Handoff compatible) and an iPad 3rd Gen (not Handoff compatible), respectively.

iPod Touch 5th Gen Settings (Handoff compatible) and iPad 3rd Gen Settings (not Handoff compatible)

Handoff functionality depends on few things:

  1. An iCloud account: You must be logged in to the same iCloud account on each device you wish to use Handoff.
  2. Bluetooth LE 4.0: Handoff broadcasts activities via Bluetooth LE signals, so both the broadcasting and receiving devices must have Bluetooth LE 4.0 support.
  3. iCloud paired: Devices should have been already paired through iCloud. When you sign into your iCloud account on Handoff compatible devices, each device is paired with other Handoff compatible devices. This is where the magic happens.

At this time, make sure that you have two Handoff compatible devices running iOS 8 or later that are logged onto the same iCloud account.

User Activities

Handoff is based on the concept of a user activity, which is a stand-alone collective unit of information that can be handed off without any dependencies on any other information.

The NSUserActivity class represents an instance of a user activity. It encapsulates the state of the application in a way that can be continued on other devices in a related application.

There are three ways to interact with NSUserActivity objects:

  1. Create user activity: The originating app creates an NSUserActivity and calls becomeCurrent() on it to start the broadcasting process. Here’s a quick example:
    let activity = NSUserActivity(activityType: "com.razeware.shopsnap.view")
    activity.title = "Viewing"
    activity.userInfo = ["shopsnap.item.key": ["Apple", "Orange", "Banana"]]
    self.userActivity = activity;
    self.userActivity?.becomeCurrent()
    

    You can use the userInfo dictionary of NSUserActivity to pass native data types or NSCoding-compliant custom objects to the receiving device. Native data types include NSArray, NSData, NSDate, NSDictionary, NSNull, NSNumber, NSSet, NSString, NSUUID, and NSURL. Passing NSURL can be a bit tricky; check out the Best Practices section of this tutorial before using NSURL with Handoff.

  2. Update user activity: Once an instance of NSUserActivity becomes current, the OS periodically invokes updateUserActivityState(activity:) on your topmost view controller to give you a chance to update the user activity. Here’s an example:
    override func updateUserActivityState(activity: NSUserActivity) {
      let activityListItems = // ... get updated list of items
      activity.addUserInfoEntriesFromDictionary(["shopsnap.item.key": activityListItems])
      super.updateUserActivityState(activity)
    }
    

    Notice that you don’t set userInfo to a new dictionary or update it directly. Instead, you should use the convenience method addUserInfoEntriesFromDictionary().

    Later in this tutorial, you’ll learn how to force the user activity to refresh on demand, or how to get a similar callback at the app delegate level.

  3. Receive user activity: When your receiving app launches with a user activity from Handoff, the app delegate calls application(:willContinueUserActivityWithType:). Note this method is not passed an instance of NSUserActivity, because it takes a while until Handoff downloads and transfers the NSUserActivity data to your app.Later on, the following delegate callback invokes once the user activity has been downloaded:
    func application(application: UIApplication, 
                     continueUserActivity userActivity: NSUserActivity,
                     restorationHandler: (([AnyObject]!) -> Void)) 
                     -> Bool {
      
      // Do some checks to make sure you can proceed
      if let window = self.window {
        window.rootViewController?.restoreUserActivityState(userActivity)
      }
      return true
    }
    

    You can then use the data stored in the NSUserActivity object to re-create the user’s activity. This is where you will update your app so that it can continue the associated activity.

Activity Types

When you create a user activity, you must specify an activity type for the activity. An activity type is simply a unique string, usually in reverse DNS notation, like com.razeware.shopsnap.view.

Each app that is capable of receiving a user activity must declare the activity types that it will accept. This is much like declaring the URL schemes your app supports. For non-document based apps, activity types are listed under the NSUserActivityTypes key at the top level of Info.plist as shown below:

Setting NSUserActivityTypes in the Info.plist of a non-document based app

For an app to support a given activity, there are three requirements:

  • Same team. Both apps must originate from the same developer with the same developer Team ID.
  • Same activity type. The receiving app must have a NSUserActivityTypes entry for the activity type created by the sending app.
  • Be signed. Both apps must be distributed through the App Store or be signed with a Developer certificate.

Now that you’ve learned about the basics of user activities and activity types, let’s dive in to an example!

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.

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!

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:

image22-69

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.

Handoff Best Practices

Before you go, I thought I’d leave you with a few thoughts on Handoff best practices.

  • NSURL: Using NSURL in an NSUserActivity userInfo dictionary can be tricky. The only NSURLs you can pass safely in Handoff are web site URLs that use HTTP or HTTPS and iCloud documents. You can’t pass local file URLs as the receiver won’t translate and map the URL properly at the receiver’s end. The best way to achieve file links is to pass a relative path and re-construct your URL on the receiving side.
  • Platform specific values: Avoid using platform specific values like the content offset of a scroll view; it’s always better to use relative landmarks. For example, if your user is viewing some items in a table view, pass the index path of the top most visible item in the table view in your user activity object instead of passing the content offset or visible rect of the table view.
  • Versioning: Think about using versioning and future-proofing updates of your app. You could add new data formats or remove values entirely from your userInfo dictionary in future versions of the app. Versioning gives you more control over how user activities are actioned in current and future versions of your app.

Where To Go From Here?

Here is the final example project that you developed in the above tutorial.

If you’re curious to learn more about Handoff, streaming, and document based Handoff, be sure to check out Apple’s Handoff Programming Guide for more information now that you know the basics.

If you enjoyed this tutorial, check out our book iOS 8 by Tutorials, which is chock-full of tutorials like this.

If you have any questions or comments about this tutorial, please join the forum discussion below!

Soheil Azarpour

My name is Soheil Azarpour and I'm an engineer, developer, author, creator, husband and father. I enjoy bicycling, boating and playing piano. I live in Manchester, NH. I create iOS apps professionally and independently. You can find me on Twitter, GitHub, Stack Overflow and LinkedIn.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 20 total!

Swift Team

... 15 total!

iOS Team

... 44 total!

Android Team

... 15 total!

macOS Team

... 11 total!

Unity Team

... 11 total!

Articles Team

... 13 total!

Resident Authors Team

... 17 total!

Podcast Team

... 3 total!

Recruitment Team

... 9 total!