Peek and Pop

Intro #

So if you’re anything like me, a few weeks ago (at time of writing) when the new iPhone 6s came out, you spent most of the day anxiously waiting for the arrival of your new phone, which you had pre-ordered the moment it was available for pre-order? One of the major new features that we now have is touch-sensitivity on the screen! And with this a new standard “Peek and Pop” gesture was introduced allowing users to press into content to see a preview, and then press harder to pop into the full detail screen. Of course, the first thing we want to do is introduce this awesome new feature to all of our apps.

Introducing Peek and Pop #

As it happens, if all we want is the standard “Peek and Pop” behaviour, similar to how Apple have it in Mail and Messages, it’s super easy to add. Especially if we already have a push-pop navigation stack such as the one we get when using a UINavigationController. Let’s look through how we can implement this, in just a few simple stages.

Enabling Force Touch #

The first thing we need to do is enable force touch on our view controller. In the view controller, that is probably using a UITableView or UICollectionView to display a list of items that the user can select to push into a detail screen for, we need to check if force touch is available on the device, and if it is, enable it for this controller.

func checkForForceTouch() {
    if #available(iOS 9.0, *) { // 1
         if traitCollection.forceTouchCapability == .available { // 2
            registerForPreviewing(with: self, sourceView: tableView) // 3
        }
    }
}
  1. Force touch is only available from iOS 9+ and the forceTouchCapability trait that we need to check is not available before that so we need to check that we are on iOS 9.
  2. UIViewControllers‘s trait collection now includes a forceTouchCapability property to indicate if it is capable of detecting force touches. We should check that it is before enabling force touch.
  3. Now that we know that we are capable of using force touch, we call the registerForPreviewingWithDelegate() method to register for previewing (peek and pop) on the controller. This method takes 2 parameters:
    • delegate: An object that conforms to UIViewControllerPreviewingDelegate that will be called when a peek and pop gesture is initiated.
    • sourceView: The view on which we want to detect force touches. Here I have assumed that the controller has a property called tableView that we want to allow previewing (peeking and popping) on.

I strongly recommend wrapping the above logic into a helper function as I have since we are going to need to call it from a few places. The first place to call it is in viewDidLoad, so that if the traits are already set our controller will be set up and ready to go straight away. However, often the forceTouchCapability trait will not be set appropriately on the controller during viewDidLoad, so we need to override traitCollectionDidChange() on our UIViewController and call it from within there too. For example:

override func viewDidLoad() {
    super.viewDidLoad()
    checkForForceTouch()
}

@available(iOS 8.0, *)
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    checkForForceTouch()
}

If you are only supporting iOS 8+ you can remove @available(iOS 8.0, *) from the above code. Now we should be good to go.

Implementing UIViewControllerPreviewingDelegate #

The next step is to implement the two methods in UIViewControllerPreviewingDelegate that will let us control which part of the screen is highlighted, what appears in the preview and what controller gets popped in response to the user action.

previewingContext(previewingContext:viewControllerForLocation:) is the first of the delegate methods to be called, and gets called when the user first begins to press hard on the screen. Think of it like the .began phase of a UIGestureRecognizer. In this method we need to define the rect in the source view to be highlighted and return a UIViewController to be used in the peek preview.

@available(iOS 9.0, *)
extension MyViewController: UIViewControllerPreviewingDelegate { // 1

    func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
        guard
            let indexPath = tableView.indexPathForRow(at: location),
            let cell = tableView.cellForRow(at: indexPath)
        else { return nil } // 2
        let previewController = DetailController() // 3
        previewController.preferredContentSize = .zero // 4
        previewingContext.sourceRect = cell.frame // 5
        return previewController // 6
    }

}
  1. After some playing around, I decided that I quite like to implement these methods as an extension to my view controllers. This way the extension can be marked as only available on iOS 9+ and it keeps the peek and pop logic nicely encapsulated.
  2. We use a binding guard to ensure that the point at which the user is touching is valid and on a cell in our table view (again, I assume here that MyViewController has a tableView property of type UITableView). We will use the cell in a moment to define the highlight rect.
  3. We create an instance of our DetailController (the one that would have been pushed in had the user simply tapped on the cell). If you wish, you can of course have a completely different view controller for the preview, but for simplicity here we will use the same one.
  4. If we want we can specify a preferred size for the preview, this may be handy if the preview is not a full screen of data. In our case, since we are previewing a controller that would have just been pushed into the navigation controller, and so would have taken the full screen, we specify CGSize.zero which lets the framework size the preview to fill the available space.
  5. We need to set the previewingContext’s sourceRect to the a rect in the sourceView’s (the one we specified when registering earlier) coordinate system, and this will be the rect that is used for the highlighted area.
  6. Return the previewController so that it can be used in the preview. If we return nil from this method the peek and pop will not happen, the gesture will be cancelled (this happens in the else clause of our guard if there is no valid cell for the touch location).

The second delegate method to implement is previewingContext (previewingContext: commitViewController:). This method gets called when the user has already seen the preview and has pressed harder to “pop” into the content. It passes us the controller that was used for the preview and we are expected to update the UI state as we want it to be after the pop. Since we are using the same view controller for the preview and the detail content, this will be very simple. We just need to add this method to our extension:

func previewingContext(_ previewingContext: UIViewControllerPreviewing, commitViewController viewControllerToCommit: UIViewController) {
    navigationController?.pushViewController(viewControllerToCommit, animated: true) // 1
}
  1. All we have to do is take the controller that we are passed (the one that was used in the preview) and push it onto our navigation controller. The framework will make a appropriate pop animation and we will be in the final state the same as if the user had tapped the cell.

Preview Action Items #

Now that we have a functioning peek and pop system there is one more thing that we can add. Preview action items. If you’ve used peek and pop in Mail or Messages you will likely have encountered these. These are the action items that appear if you swipe up while peeking at content. Messages gives us access to quick replies and Mail gives us “Reply”, “Forward”, “Mark…” etc. It is handy to provide actions that a user might want to perform on content without having to pop fully into it, such as favouriting, adding to a reading list or sharing.

To get custom actions in the previewing mode in our own apps we need to override the previewActionItems() variable on the UIViewController subclass that we are using in our preview. This is the controller that we instantiated and returned in the first delegate method above.

@available(iOS 9.0, *) // 1
override var previewActionItems: [UIPreviewActionItem] { // 2
    let action = UIPreviewAction(title: "My Action", style: .Default) { (selectedAction, controller) in // 3
        print("My Action selected") // 4
    }
    // 5
    return [action] // 6
}
  1. Again, mark this variable as iOS 9+ only.
  2. Override the previewActionItems() method which must return an array of UIPreviewActionItems to be displayed when previewing.
  3. Create an instance of UIPreviewAction (a subclass of UIPreviewActionItem) specifying a title for the button, a display style (one of .default, .selected or .destructive) and a trailing closure that will be called when the user selects the action. The callback will be passed the UIPreviewAction instance that was selected and the controller that was being previewed.
  4. For simplicity this callback just logs that the action was selected.
  5. Possibly want to create some additional actions here?
  6. Return an array of the actions that we want displayed.

This method is called every time that a peek and pop action is started so if we want we could dynamically adjust the actions available based on the content that we are previewing.

Example #

While the tutorial walks through the building blocks required to setup a peek and pop system it is sometimes nice to see a full working example. As such, do checkout the full working example that I have pieced together which you can download here.

Conculsion #

Peek and pop is a pretty cool new feature from Apple and I’m pretty excited to see what sorts of things people come up with to use it for. There are of course other APIs for the new force touch which I’ll cover in a future post (app quick actions, and just being able to detect the force of a touch for our own gestures and actions) but this should get you started if you want to introduce this feature into your own apps.

As ever do get in touch if you have any queries about the content of this post, if you have any issues implementing your own version of this, or if you think anything could be better clarified.

 
3
Kudos
 
3
Kudos

Now read this

Mastering GCD - The Basics

Intro # The next few posts are aimed at really getting to grips with Apple’s GCD (Grand Central Dispatch), understanding what it does, how to use it, and most importantly when and why we might need to use it. If you’ve read the previous... Continue →