Chapter 12. Supporting the iOS Ecosystem

In this chapter, we’ll add support for sharing, handoffs (so users can resume what they’re doing on other iOS devices or in the OS X app), and search (so the iOS search system can be used to find text within note documents). All three of these features help to integrate your app into the wider context of the user’s phone, which means that your app is no longer an island.

Sharing with UIActivityController

We’ll start by adding sharing support to the image attachment view controller, as shown in Figure 12-1.

lesw 1201
Figure 12-1. The standard iOS share sheet

Sharing on iOS is handled by UIActivityViewController, which provides a standard view controller offering system services, such as copy, paste, and so on, as well as sharing to social media, email, or text messaging. Other apps can also provide share destinations.

  1. Open Main.storyboard and go to the image attachment view controller.

  2. Add a UIToolBar from the Object library to the view and place it at the bottom of the screen. This will also include a UIBarButtonItem, which works pretty much exactly like our old friend UIButton, but is customized to work in toolbars.

  3. Resize the toolbar to make it fit the width of the screen. Next, click on the Pin menu, and pin the left, right, and bottom edges of the view. This will keep it at the bottom of the screen and make it always fill the width of the screen.

  4. Select the button and set its System Item property to Action, as shown in Figure 12-2. This will change its icon to the standard iOS share icon.

    lesw 1202
    Figure 12-2. Setting the button to the Action mode
  5. Open ImageAttachmentViewController.swift in the Assistant editor.

  6. Hold down the Control key and drag from the toolbar button you just added into ImageAttachmentViewController. Create a new action called shareImage.

  7. Add the following code to the shareImage method. Note that the type for the sender parameter is UIBarButtonItem—you’ll need to change it when you start writing the code:

  @IBAction func shareImage(sender: UIBarButtonItem) {

      // Ensure that we're actually showing an image
      guard let image = self.imageView?.image else {
          return
      }

      let activityController = UIActivityViewController(
          activityItems: [image], applicationActivities: nil)

      // If we are being presented in a window that's a regular width,
      // show it in a popover (rather than the default modal)
      if UIApplication.sharedApplication().keyWindow?.traitCollection
          .horizontalSizeClass == UIUserInterfaceSizeClass.Regular {
          activityController.modalPresentationStyle = .Popover

          activityController.popoverPresentationController?
              .barButtonItem = sender
      }

      self.presentViewController(activityController, animated: true,
          completion: nil)

  }

When the share button is tapped, we want to prepare and present a UIActivityController, which will allow the user to do something with the image. What that something actually is depends upon the capabilities of the system and the apps that the user has installed. To create it, you pass in an array of activityItems, which can be a wide variety of things: URLs, images, text, chunks of data, and so on. The UIActivityController will then determine what services can accept these items, and then let the user choose what to do.

When the app is being presented in a larger screen, such as on an iPhone 6+ or iPad, we want to show it as a popover. To detect this, we ask the window in which the app is running to tell us about its horizontal size class—that is, whether it is in a horizontally “compact” view, or a horizontally larger “regular” view. If it’s in a regular-sized view, we instruct the activity controller to use a popover, and we set the barButtonItem property on the popoverPresentationController to the sender, which will visually connect the popover to the share button in the toolbar.

Handoffs

Let’s imagine that your user’s on a bus, tapping out a note. She arrives at her stop, gets off the bus, and walks into the office, still writing the note. Eventually, she reaches her desk, and she wants to finish up the note. She could finish it up on the phone, but she’s right in front of a dedicated workstation. Rather than deal with a tiny touchscreen, she instead uses Handoff to move her work from her phone to the desktop.

Handoff is a technology on the Mac, iOS, and on watchOS that allows the user to start an activity on one device and seamlessly move to another device (see Figure 12-3). The way it works is this: applications register activity types with the system, which are simple text strings that are the same across all of the different apps that can receive the handoff. When the user opens a document, he marks it as the current activity; this makes the operating system broadcast this fact to all nearby devices. When the user decides to activate Handoff on another device, the originating device and the receiving device quickly swap information about what he wants to do, and the receiving device’s app delegate is then given the opportunity to continue the activity.

lesw 1203
Figure 12-3. Handoffs working with Safari on iOS and OS X
Note

Because we’re using NSDocument and UIDocument, lots of the details of this get taken care of for you. If you weren’t using the document system, you’d need to manually create your own NSUserActivity objects before calling becomeCurrent. For more information, see the Handoff Programming Guide in the Xcode documentation.

To get started using Handoff, we need to describe to the system the type of “activity” that is associated with editing this document. When we do this, the device will inform all other devices that belong to the same person that this specific document is being edited.

  1. Select the project at the top of the Project Navigator (Figure 12-4).

    lesw 1204
    Figure 12-4. Selecting the project in the Project Navigator
  2. Go to the Notes target settings (that is, the OS X app) and scroll down to the Document Types section.

  3. Add a new entry in “Additional document type properties” by expanding the “Additional document type properties” triangle, selecting the CFBundleTypOSTypes entry, and clicking the + button that appears.

  4. Call the new entry NSUbiquitousDocumentUserActivityType and set its type to String. Set its value to au.com.secretlab.Notes.editing.

  5. Now go to the same place in Notes-iOS, and add the same entry.

    Warning

    If you have been using a custom bundleID throughout, make sure you use that here with .editing appended at the end. If you don’t do this, handoffs will not work.

    Once you’ve done this, the two applications will associate a Handoff-able activity with their document types. When the document is open, the app will be able to simply say to the system, “Begin broadcasting the fact that this document is open.”

  6. Open the AppDelegate.swift file that belongs to the Notes-iOS target (not the OS X one!).

  7. Implement the following method, which returns to the list of documents and then signals that that view controller should resume an activity:

      func application(application: UIApplication,
          continueUserActivity userActivity: NSUserActivity,
          restorationHandler: ([AnyObject]?) -> Void) -> Bool {
    
          // Return to the list of documents
          if let navigationController =
              self.window?.rootViewController as? UINavigationController {
    
              navigationController.popToRootViewControllerAnimated(false)
    
              // We're now at the list of documents; tell the restoration
              // system that this view controller needs to be informed
              // that we're continuing the activity
              if let topViewController = navigationController.topViewController {
                  restorationHandler([topViewController])
              }
    
              return true
          }
          return false
      }

    The continueUserActivity method is called when the user has decided to hand off the activity from one device to the next.The userActivity object contains the information describing what the user wants to do, and this method is responsible for telling the app what needs to happen to let the user pick up from where the last device left off.

    It does this through the restorationHandler closure that it receives as a parameter. This closure takes an array of objects which the app should call the restoreUserActivityState method on; this method receives the NSUserActivity as a parameter, which can be used to continue the state.

    The reason for doing this is to move as much of the logic that drives the continuation of the activity to the view controllers, instead of making the app delegate have to know about the details of how documents get opened.

    The way that we’ll handle this in this app is to return to the DocumentListViewController, and then indicate that the view controller should be told about the handoff by passing it to the restorationHandler.

  8. Open DocumentListViewController.swift.

  9. Add the following method to the DocumentListViewController class:

      override func restoreUserActivityState(activity: NSUserActivity) {
          // We're being told to open a document
    
          if let url = activity.userInfo?[NSUserActivityDocumentURLKey]
          as? NSURL {
    
              // Open the document
              self.performSegueWithIdentifier("ShowDocument", sender: url)
          }
    
      }

    This method is called as a result of passing the DocumentListViewController to the restorationHandler in continueUserActivity. Here, we extract the URL for the document that the user wants to open by getting it from the NSUserActivity’s userInfo dictionary, and then performing the ShowDocument segue, passing in the URL to open. This means that when the application is launched through the Handoff system, the document list will immediately open the document that the user wants.

  10. Finally, add the following code to the viewWillAppear method of DocumentViewController, to make the activity current:

      // If this document is not already open, open it
      if document.documentState.contains(UIDocumentState.Closed) {
          document.openWithCompletionHandler { (success) -> Void in
              if success == true {
                  self.textView?.attributedText = document.text
    
                  self.attachmentsCollectionView?.reloadData()
    
    >             // We are now engaged in this activity
    >             document.userActivity?.becomeCurrent()
    
                  // Register for state change notifications
                  self.stateChangedObserver = NSNotificationCenter
                      .defaultCenter().addObserverForName(
                          UIDocumentStateChangedNotification,
                          object: document,
                          queue: nil,
                          usingBlock: { (notification) -> Void in
                          self.documentStateChanged()
                      })
    
                  self.documentStateChanged()
    
              }

    Every UIDocument has an NSUserActivity. To indicate to the system, and to every other device that the user owns, that the user’s current task is editing this document, we call becomeCurrent on the document’s userActivity. This causes the current device to broadcast to all other devices in range, letting them know that we’re offering to hand off this activity.

    You can now test handoffs. Launch the iOS app on your phone, and then launch the OS X app. Open a document on your phone, and a Handoff icon will appear at the left of the dock on your Mac, as shown in Figure 12-5.

    lesw 1205
    Figure 12-5. Handoff on OS X

    The reverse will also work on iOS: open a document on your Mac, and the iOS app’s icon will appear on the lock screen (Figure 12-6).

    lesw 1206
    Figure 12-6. Handoff on iOS—the handoff icon is shown in the bottom-left corner

Searchability

The next feature we’ll add is the ability for users to search the phone to find documents that they’ve written. There are three different searching technologies that we can use to support this: using NSUserActivity objects, Core Spotlight, and web indexing.

Note

Because we’re not building web apps in this book, we won’t be covering web archiving. If you’re interested in it, you can read more about it in the App Search Programming Guide, in the Xcode documentation.

We’ll be covering marking NSUserActivity objects as searchable in this chapter. In the next chapter, which covers creating extensions, we’ll also talk about creating a Spotlight indexing extension, which provides additional search functionality by registering the contents of all documents in the app with Core Spotlight.

We’ll start by adding support for indexing the app through NSUserActivity.

  1. Open DocumentViewController.swift.

  2. Import the Core Spotlight framework at the top of the file:

      import CoreSpotlight
  3. Update the viewWillAppear method to add searchable metadata to the document’s user activity when the document is opened:

      // If this document is not already open, open it
      if document.documentState.contains(UIDocumentState.Closed) {
          document.openWithCompletionHandler { (success) -> Void in
              if success == true {
                  self.textView?.attributedText = document.text
    
                  self.attachmentsCollectionView?.reloadData()
    
    >             // Add support for searching for this document
    >             document.userActivity?.title = document.localizedName
    >
    >             let contentAttributeSet
    >                 = CSSearchableItemAttributeSet(
    >                     itemContentType: document.fileType!)
    >
    >             contentAttributeSet.title = document.localizedName
    >             contentAttributeSet.contentDescription = document.text.string
    >
    >             document.userActivity?.contentAttributeSet
    >                 = contentAttributeSet
    >
    >             document.userActivity?.eligibleForSearch = true
    
                  // We are now engaged in this activity
                  document.userActivity?.becomeCurrent()
    
                  // Register for state change notifications
                  self.stateChangedObserver = NSNotificationCenter
                      .defaultCenter().addObserverForName(
                          UIDocumentStateChangedNotification,
                          object: document,
                          queue: nil,
                          usingBlock: { (notification) -> Void in
                          self.documentStateChanged()
                      })
    
                  self.documentStateChanged()
    
              }

    This code adds further metadata to the document’s userActivity. First, it provides a name for the document, which will appear in the Spotlight search results. In addition, we create a CSSearchableItemAttributeSet, which is the (overcomplicated) term for “stuff the search system uses to decide if it’s what the user’s looking for.” In this case, we provide two pieces of information: the name again, and the text of the document.

    We then provide this to the userActivity and mark it as available for searching.

You can now test searching. Run the app and open a document. Type some words into the document, close the app, and go to the Search field (swipe down while on the home screen). Type in some of the words that you added to the document, and your document will appear! When you tap on the search result, the app will launch, and you’ll be taken to the document.