The second thing to write on 3D Touch would definitely have to be Peek and Pop. Over the past few days, I've come to fall in love with this feature for the mere reason that I often never want to commit to clicking on a link and waiting for the next page to load. In fact, half the time, what I'm peeking at is really all I cared to see anyways! Be it a full size photo in the Photos app, or figuring out if an email is worth reading or not, I personally don't see an excuse to not include this in your app!

There are three parts to knowing everything you need to know about incorporating peeking and popping in your app. There's peeking, preview actions, and popping. Here's what it looks like:

Peeking

Peeking is the entry point of this nifty little feature. It essentially gives you a snapshot of the next view controller you're going to be looking at when it's activated. It's also the more difficult feature of 3D Touch to implement because of all the moving parts. Boiled down, getting a peek to happen consists of several parts:

  • Registering the object that handles the peek.
  • Conforming to the UIViewControllerPreviewingDelegate protocol
  • Determining which view controller to present for "peeking" purposes.
  • Getting the right size for your "peeking" view controller.
  • Unregistration of the object that handles the peek. (Thankfully the system handles this for you when the registered object is deallocated!)

First off, we need to let the system know that we have a view that wants to participate in 3D Touch. Since we need to register an actual view, I would suggest registering as early as viewDidLoad. The function to register looks like this:

override func viewDidLoad() {
    super.viewDidLoad()

    //The 'view' argument should be the view receiving the 3D Touch.
    registerForPreviewingWithDelegate(self, sourceView: view)
}

The cool thing about this function though, is that you can call it multiple times and pass it multiple views for a single view controller. However, there is one caveat. From the docs:

You can designate more than one source view for a single registered view controller, but you cannot designate a single view as a source view more than once.

When the 3D Touch event is actually executed at runtime, the only function it calls is this:

func previewingContext(previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
    //Do yo thang, my friend.
}

The UIViewControllerPreviewing context passed in has a settable sourceRect. By default, that sourceRect is the bounds of the sourceView that you provided to the registerForPreviewingWithDelegate: function. The contents of that sourceRect inside of the sourceView will not blur while the rest of the screen does at the start of your peek. In this screenshot, I provided the entire ViewController's view as the source view and gave it an arbitrary sourceRect inside of the previewingContext:viewControllerForLocation: function. You can see that the sourceRect is independent of any particular view ands serves as a clipping rect more than anything. First the code:

override func viewDidLoad() {
    super.viewDidLoad()

    registerForPreviewingWithDelegate(self, sourceView: view)
}

func previewingContext(previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
    previewingContext.sourceRect = CGRect(origin: CGPoint(x: greenSquare.frame.origin.x - 75.0, y: greenSquare.frame.origin.y - 75.0), size: CGSize(width: 150.0, height: 150.0))
    return UIViewController()
}

And now, the result:

Whilst coding your peeking functionality, it may seem that it may be harder to achieve with UITableViews and UICollectionViews because of the caveat we outlined above. Since you can't register a view more than once, you can't just keep calling registerForPreviewingWithDelegate in your cellForRow: functions. Doing so will result in a crash if a cell was already previously registered and gets reused.

However, Apple thought of all of this ahead of time. This is where that second parameter becomes of any use. As long as you provide your tableView or collectionView as the argument to registerForPreviewingWithDelegate:, then you can use the second location parameter of the previewingContext: function to your advantage. That location parameter is the location of your touch within your sourceView at the beginning of your force touch. Both UITableView and UICollectionView have location API's that will give you the sourceRect of a cell at a particular location in the tableView or collectionView. Here's an example of providing the proper clipping rect in our previewingContext: delegate function:

UITableView Snippet

override func viewDidLoad() {
    super.viewDidLoad()

    registerForPreviewingWithDelegate(self, sourceView: tableView)
}

func previewingContext(previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
    if let indexPath = tableView.indexPathForRowAtPoint(location) {
        //This will show the cell clearly and blur the rest of the screen for our peek.
        previewingContext.sourceRect = tableView.rectForRowAtIndexPath(indexPath)
        return viewControllerForIndexPath(indexPath)
    }
    return nil
}

UICollectionView Snippet

override func viewDidLoad() {
    super.viewDidLoad()

    registerForPreviewingWithDelegate(self, sourceView: tableView)
}

func previewingContext(previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
    if let indexPath = collectionView.indexPathForItemAtPoint(location), cellAttributes = collectionView.layoutAttributesForItemAtIndexPath(indexPath) {
        //This will show the cell clearly and blur the rest of the screen for our peek.
        previewingContext.sourceRect = cellAttributes.frame
        return viewControllerForIndexPath(indexPath)
    }
    return nil
}

Popping

By far, popping is the easiest thing to implement. To do so, we have one more function to learn about in the UIViewControllerPreviewingDelegate protocol:

func previewingContext(previewingContext: UIViewControllerPreviewing, commitViewController viewControllerToCommit: UIViewController) {
    //Here's where you commit (pop)
}

In this function, the only parameter we really care about is the viewControllerToCommit parameter. This parameter directly correlates to the UIViewController instance that you return in the previewingContext:viewControllerForLocation: function that we talked about in the "peeking" section.

In this function, you want to make sure you call whatever function you normally would to present the view controller you are try to "pop" into. As you can guess, you would use UIViewController's presentViewController:animated:completion: function or UINavigationController's pushViewController:animated: function.

And that's it! Here's a snippet of what it looks like after all of that:

func previewingContext(previewingContext: UIViewControllerPreviewing, commitViewController viewControllerToCommit: UIViewController) {
    presentViewController(viewControllerToCommit, animated: true, completion: nil)
}

And that's really all there is to peeking and popping. There is one more thing to talk about but it's totally optional.

UIPreviewActionItems

UIPreviewActions are simply an array of items that conform to the UIPreviewActionItem protocol. These items represent actions that can be taken for a peeking view controller when swiping up on the peek before popping. UIPreviewActions are created and handled in the view controller that you are trying to present. You do so by overriding that UIViewController's previewActionItems() function like so:

override func previewActionItems() -> [UIPreviewActionItem] {
    return []
}

Within this function, you want to return an array of any object that conforms to the UIPreviewActionItem protocol:

@available(iOS 9.0, *)
public protocol UIPreviewActionItem : NSObjectProtocol {
    public var title: String { get }
}

If you manage to correctly return an array of UIPreviewActionItems, then you'll be rewarded with a little something that looks like this!

Thankfully, you don't actually need to create your own object's that conform to the UIPreviewActionItem protocol in order to achieve this. Apple already gave us two classes to use out of the box: UIPreviewAction and UIPreviewActionGroup.

UIPreviewAction

UIPreviewActions are the UIAlertSheet style cells that you see below a peeking view controller when you swipe up during a peak. They are singular style cells that execute a closure after tapping them. They come in three styles: Default, Selected, and Destructive. Much like UIAlertSheets, I believe we're starting to see a pattern here, huh?

A UIPreviewAction is constructed almost exactly as you would a UIAlertAction for a UIAlertController. It's essentially just a wrapper around a closure so you can execute some code when the action is executed. Here's how you create a UIPreviewAction:

let action = UIPreviewAction("Press Me!", style: .Default) { (action, viewController) in 
    print("I believe I can fly")
}

UIPreviewActionGroup

Action groups are exactly as they sound. They encapsulate multipleUIPreviewActions. Instead of one cell that executes a closure after tapping it, a UIPreviewActionGroup shows an expandable cell that expands into multiple subcell's when tapped. Initializing one of these is super simple too:

let actionGroup = UIPreviewActionGroup("Look at me, I can grow!", style: .Default, actions: [action1, action2, action3])

You can also get crazy with this too. Since UIPreviewActionGroup is just a subclass of UIPreviewAction and the action group is initialized with an array of UIPreviewActions, you can actually initialize an action group with other action groups too! And then you can initialize those action groups with action groups, and the cycle never ends.

Webview Peek and Pop

Just like in Safari, you can also enable your UIWebView and WKWebviews to support peek and pop! It's super easy too:

webView.allowsLinkPreview = true

And that's it! SUPER easy. WKWebViews and UIWebViews both have this property and they are both set to false by default. As you may already know, you can configure your web views to detect different data types, and allowing peek and pop on your web view's allows you to bring up a different peek for each data type. For example, a link that is an Address will bring up a peeking map view.

Storyboard Peek and Pop

As of Xcode 7.1, you can even implement peek and pop in your storyboards! By creating commit and action segues, you can link to them in your storyboards to integrate peek and pop without any code whatsoever! Thanks to Quinn at Apple for pointing this out on Twitter!

Conclusion

Peek and Pop isn't that hard to implement once you get the hang of it. The more and more I play with it, the more I realize that peek and pop can do so much for your app. Just like our shortcut icons on the home screen, I can see peek and pop used in many places in your app. Hopefully, this giude wasn't too confusing for you either. As is usual, feel free to comment below if you have questions or if you want to see something here that I haven't included yet! And as always,

Happy coding, fellow nerds!