Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

3D Touch in Swift: Implementing Peek and Pop

DZone's Guide to

3D Touch in Swift: Implementing Peek and Pop

Learn to utilize the new 3D Touch Peek and Pop features on the new iPhone 6S using Swift 2.

· Mobile Zone
Free Resource

Download this comprehensive Mobile Testing Reference Guide to help prioritize which mobile devices and OSs to test against, brought to you in partnership with Sauce Labs.

One of my favorite features on the iPhone 6s is the new 3D Touch Peek and Pop. Peek and Pop relies on pressure sensitivity to offer the user a transient preview pop up with a press (peek) or allows them to navigate that item with a deeper press (pop). Both give a atisfying haptic click and peeking can also display a set of context sensitive preview actions.

It so happens that over the last week or so, I've been updating my PHImageManager based photo browser. This is a project I started back in January and I wanted to update to Swift 2 to include in the next version of Nodality. Although Nodality is an iPad application, the component is universal and is the perfect candidate for my first foray into Peek and Pop.

Interaction Design

For non 3D Touch devices, my photo browser's interaction design is pretty simple, the top segmented control allows the user to navigate between their collections and touching an image selects it and returns control back the the host application. With a long press, the user can toggle the favourite status of an item via a UIAlertController.

For 3D Touch devices, the user can click on an image to pop up a peek preview and then toggle the favourite status via a UIPreviewAction. A deeper pop selects an image and returns control to the host application.

Setting Up

I don't want to lose the long press functionality for non 3D Touch devices, so during initialisation, I look at the traitCollection o either register the photo browser for peek previewing or implement a long press gesture recogniser. Because the component itself is modally presented, its traitCollection claims not to be force touch enabled, so I look at the raitCollection of the application's key window:

    if UIApplication.sharedApplication().keyWindow?.traitCollection.forceTouchCapability == UIForceTouchCapability.Available
    {
        registerForPreviewingWithDelegate(self, sourceView: view)
    }
    else
    {
        let longPress = UILongPressGestureRecognizer(target: self, action: "longPressHandler:")
        collectionViewWidget.addGestureRecognizer(longPress)

    }

Peeking

To register the photo browser for previewing with delegate, it must implement UIViewControllerPreviewingDelegate and for peeking, previewingContext(viewControllerForLocation) is called. Here I simply need to return an instance of the view controller I want to act as the preview. When the user touched an image, I instantiated a tuple named touchedCel of type (UICollectionViewCell, NSIndexPath) that refers to the touched image and with that I can get the PHAsset required for previewing and hand it to my PeekViewController:

    func previewingContext(previewingContext: UIViewControllerPreviewing,
        viewControllerForLocation location: CGPoint) -> UIViewController?
    {
        guard let touchedCell = touchedCell,
            asset = assets[touchedCell.indexPath.row] as? PHAsset else
        {
            return nil
        }

        let previewSize = min(view.frame.width, view.frame.height) * 0.8

        let peekController = PeekViewController(frame: CGRect(x: 0, y: 0,
            width: previewSize,
            height: previewSize))

        peekController.asset = asset

        return peekController

    }

The previewing view controller, PeekViewController, reuses ImageItemRenderer - the same item renderer as my main UICollectionView, so all the code for requesting a thumbnail sized image was already available:

    class PeekViewController: UIViewController
    {
        let itemRenderer: ImageItemRenderer

        required init(frame: CGRect)
        {
            itemRenderer = ImageItemRenderer(frame: frame)

            super.init(nibName: nil, bundle: nil)

            preferredContentSize = frame.size

            view.addSubview(itemRenderer)
        }

        var asset: PHAsset?
        {
            didSet
            {
                if let asset = asset
                {
                    itemRenderer.asset = asset;
                }
            }
        }

    }

Adding the preview action to toggle the favourite status of the asset it a simple as returning an array of UIPreviewActionItem. PeekViewController already knows what asset needs to be toggled and the shared photo library is a singleton, so the code is just:

    var previewActions: [UIPreviewActionItem]
    {
        return [UIPreviewAction(title: asset!.favorite ? "Remove Favourite" : "Make Favourite",
            style: UIPreviewActionStyle.Default,
            handler:
            {
                (previewAction, viewController) in (viewController as? PeekViewController)?.toggleFavourite()
            })]
    }

    func toggleFavourite()
    {
        if let targetEntity = asset
        {
            PHPhotoLibrary.sharedPhotoLibrary().performChanges(
                {
                    let changeRequest = PHAssetChangeRequest(forAsset: targetEntity)
                    changeRequest.favorite = !targetEntity.favorite
                },
                completionHandler: nil)
        }

    }

The final result, with very little code, is a fully functioning "peek" with the nice system animation and that satisfying haptic click:

Popping

Popping is easier still. Here, I implement the previewingContext(commitViewController) method of UIViewControllerPreviewingDelegate. My photo browser has a method named requestImageForAsset() which requests the image for an asset and then dismisses the browser, so I simply invoke that:

    func previewingContext(previewingContext: UIViewControllerPreviewing,
        commitViewController viewControllerToCommit: UIViewController)
    {
        guard let touchedCell = touchedCell,
            asset = assets[touchedCell.indexPath.row] as? PHAsset else
        {
            dismissViewControllerAnimated(true, completion: nil)

            return
        }

        requestImageForAsset(asset)
    }

Conclusion

I've only had my phone for a matter of hours and already I find peeking and popping a very natural way of interacting with it. Considering the simplicity with which peek and pop can be implemented, I'd humbly suggest that adding it to your own applications is a pretty big win!

As always, the source code for this project is available at my GitHub repository here. Enjoy!

Analysts agree that a mix of emulators/simulators and real devices are necessary to optimize your mobile app testing - learn more in this white paper, brought to you in partnership with Sauce Labs.

Topics:
mobile ,ios ,swift 2 ,3d touch ,peek and pop

Published at DZone with permission of Simon Gladman, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}