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 3 of 5 of this article. Click here to view the first page.

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.

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.