UIPreviewInteraction


So this last week, I was at WWDC and I had the time of my life. I met some old friends, made some new ones, and was finally able to put some faces to everyone I follow on Twitter! The Keynote was pretty awesome, but the things that caught my eye the most were the updates to 3D Touch. What better fodder to use for my blog than to update my 3D Touch series? My friend, @smileyborg, took the stage at WWDC and during that session I learned about something pretty cool: UIPreviewInteraction.


UIPreviewInteraction

UIPreviewInteraction is a new class that allows you to have a more fine-grained control over what you can do with 3D Touch. Quite different from the force APIs on UITouch, UIPreviewInteraction is more than just a property on a class. However, it is just as easy to implement as the Peek and Pop APIs!

UIPreviewInteraction is a new class in iOS 10 (iOSX? 😜) that allows you to plug in to the progress of a 3D Touch action on any view that you specify. Progress goes from 0.0 to 1.0 the harder you press on the screen. The other interesting thing here, is that you also get two separate calls that correspond to both peeking and popping. Since "peeking" and "popping" are "preview" features, this means you can further interact with both of them and execute code parallel to each action. It also has the same force processing as Peek and Pop, with nifty, automatic haptic feedback.

The first thing that is required, to plug into either peeking or popping, is to create an instance of UIPreviewInteraction and set the delegate of the UIPreviewInteraction instance. A good place to do this is in viewDidLoad():

class KrakenViewController: UIViewController, UIPreviewInteractionDelegate {
    private var planPreviewInteraction: UIPreviewInteraction!

    override func viewDidLoad() {
        super.viewDidLoad()

        //Let's get a preview into the Kraken's most Diabolical Plans®
        planPreviewInteraction = UIPreviewInteraction(view: view)
        planPreviewInteraction.delegate = self
    }
}

The delegate property on UIPreviewInteraction is a UIPreviewInteractionDelegate. After this code is executed, any 3D Touch on the view of KrakenViewController will start calling functions on the UIPreviewInteractionDelegate.

UIPreviewInteractionDelegate

Most of the magic, of custom 3D Touch code, is done through the new, fancy-schmancy UIPreviewInteractionDelegate. Have a look at the declaration:

public protocol UIPreviewInteractionDelegate : NSObjectProtocol {
    @available(iOS 10.0, *)
    public func previewInteraction(_ previewInteraction: UIPreviewInteraction, didUpdatePreviewTransition transitionProgress: CGFloat, ended: Bool) // transitionProgress ranges from 0 to 1

    @available(iOS 10.0, *)
    public func previewInteractionDidCancel(_ previewInteraction: UIPreviewInteraction)

    @available(iOS 10.0, *)
    optional public func previewInteractionShouldBegin(_ previewInteraction: UIPreviewInteraction) -> Bool

    // If implemented, a preview interaction will also trigger haptic feedback when detecting a commit (pop). The provided transitionProgress ranges from 0 to 1.
    @available(iOS 10.0, *)
    optional public func previewInteraction(_ previewInteraction: UIPreviewInteraction, didUpdateCommitTransition transitionProgress: CGFloat, ended: Bool)
}

UIPreviewInteractionDelegate has 2 required functions and 2 optional functions:

  • didUpdatePreviewTransition - This function is the first required function for implementing preview interactions. It is functionally equivalent to plugging into 3D Touch's peek functionality. It gives you a transitionProgress from 0.0 to 1.0, which you would typically use to start a preview transition or animation of some sort. It has an ended Boolean, which is only true when transitionProgress reaches 1.0. You can use this function to complete your preview interaction.
  • previewInteractionDidCancel - This function is the last required function for implementing preview interactions. Since all users have the capability of cancelling out of a peek, at any time, you need to handle that case. In most cases, you would probably be reversing whatever interaction you implemented in the didUpdatePreviewTransition function.
  • previewInteractionShouldBegin - This function is optional and does what it sounds like. It is called when a user attempts a 3D Touch on the view hooked up to the UIPreviewInteraction instance. By returning a value of true or false, in the implementation of this function, you can tell the system whether or not a preview interaction should begin. This returns true by default, since implementing this delegate indicates you want a preview interaction to occur.
  • didUpdateCommitTransition - This function is optional and is functionally equivalent to plugging into 3D Touch's commit functionality. It's optional because of the fact that every pop follows a peek, but every peek is not always followed by a pop. In the case of creating a preview interaction, you have the choice to prevent the user from committing their peek by not implementing this method. Just like the didUpdatePreviewTransition function, this function also gives you a transitionProgress from 0.0 to 1.0, which you would typically use to transition the interaction into a more permanent state. One example of this is an interactive UIViewController transition. This function also comes with an ended Boolean, which behaves exactly like the ended Boolean inside the didUpdatePreviewTransition function.

In case that extensive and awesome list wasn't awesome enough for you to hit the ground running, then keep reading and I'll explain how to actually put it together!

Plugging into Peek (required)

The peek functionality is a pretty neat feature. In case you still don't actually know what that means, I wrote up a nice little guide, which can be found here.

Now, I say "plugging into peek's functionality" here but what I really mean is that you can run code very similar to 3D Touch's peek feature. Instead of actually running code parallel to an implemented peek transition (although, with this API, you could very easily do that). Before, you were able to hack together your own force touch recognizer, but the issue with that is determining the appropriate thresholds for activation. Apple put a lot of thought into those thresholds already, so having access to an API that takes advantage of that is pretty cool, if you ask me.

After setting up your UIPreviewInteraction instance, implementing your very own peek functionality is as easy as conforming to UIPreviewInteractionDelegate. After overriding these two functions, your code may look a little something like this:

func previewInteraction(_ previewInteraction: UIPreviewInteraction, didUpdatePreviewTransition transitionProgress: CGFloat, ended: Bool) {
    //The ended parameter will be true with haptic feedback 
    //that occurs at the end of the peek. 

    //take advantage of the transitionProgress 
    //parameter to drive your animation or transition or 
    //whatever you're trying to do here.

    //P.S. updatePreviewAnimation() is my own function.
    updatePreviewAnimation(progress: transitionProgress)

    //ended will be true when transitionProgress reaches 1.0.
    if ended {
        //completePreviewAnimation() is just a function I created to
        //handle what happens after a completed preview.
        completePreviewAnimation()
    }
}

func previewInteractionDidCancel() {
    updatePreviewAnimation(progress: 0.0)
}

And that's it! Nothing much to see here. Just make sure to handle both the happy path and cancel cases.

Plugging into Pop (optional)

There's really not much to write about here. If you read the last section, then you probably already know what to do here:

//Change the word "didUpdatePreview" here
func previewInteraction(_ previewInteraction: UIPreviewInteraction, didUpdatePreviewTransition transitionProgress: CGFloat, ended: Bool) {
}

//to "didUpdateCommit" like so:
func previewInteraction(_ previewInteraction: UIPreviewInteraction, didUpdateCommitTransition transitionProgress: CGFloat, ended: Bool) {
}

Your logic would really be the same here. The transitionProgress parameter would still go from 0.0 to 1.0 and the ended parameter would be true when the progress reaches 1.0. The only thing you really have to remember, is that "preview" means "peek" and "commit" means "pop". The only difference is that the implementation of this function is purely optional when comforming to UIPreviewInteractionDelegate

There's really nothing else left to do. You're done! Now leave. You've already taken up too much of my time explaining this to you.

Seriously. That's it.

Go.

What are you waiting for?! Go create something, dammit. 😜

Deciding If Something Should Happen(optional)

Sometimes your program may have some sort of state that is a deciding factor on whether or not a view should begin a preview interaction or not. Much like other APIs we are familiar with (such as shouldHighlightRowAtIndexPath in UITableView), you can implement a function in UIPreviewInteractionDelegate that returns a Bool too:

func previewInteractionShouldBegin(_ previewInteraction: UIPreviewInteraction) -> Bool {
    if theKrakenWantsToHide {
        return false
    }
    return true
}

Easy enough, yo.

Conclusion

As more and more time passes, and with each WWDC, Apple continues to enrich iOS with even more APIs, which give us the capability to create amazing experiences for our wonderful users. Even more interestingly, Apple seems to continue to place an even greater importance on ease-of-use for these APIs. That lowers the barrier to entry to new user experiences. Hopefully you can take something from this post and create something truly amazing. Thanks for reading! And as always,

Happy coding, fellow nerds!