8. Pinch, Pan, & Rotate

(Note that this tutorial is a continuation of the swipe tutorials, located here and here. You may wish to complete those tutorials before attempting this one)

Cocoa Touch, as the name suggests, is a very touch-oriented interface. In fact, individual apps do not have a single hardware button available to them for user-interaction purposes–everything the user can do must be accomplished via gestures. To that end, a number of gestures (beyond Swipe) are provided out-of-the-box, including Tap (for “clicking”), “Long Press“, Pinch, Pan, and Rotate.

Tap and Long Press behave in a manner very similar to Swipe, and require much the same code to use. For practice, in fact, you can repeat the Swipe tutorials using Tap or Long Press instead.

This tutorial is not concerned with Taps or Long Presses. This tutorial is concerned with the proverbial red-headed step-children (and, by “red-headed step-children”, I, of course, mean “continuous gestures”): Pinch, Pan, and Rotate.

Continuous Gestures

Before we get into the nitty-gritty, we need some background information. There are two classes of gesture: “discrete gestures” and “continuous gestures”. The discrete gestures–Tap, Press, and Swipe–occur as a single, quick motion, so the action message for such a gesture is fired exactly once (as you may recall from the Swipe tutorial). Continuous gestures, on the other hand, send their action message repeatedly for the duration of the gesture. Each call includes additional information not relevant to discrete gestures, but very important to continuous gestures: a delta value (scale, translation, or angle for Pinch, Pan, and Rotate, respectively) and a velocity.

The delta value represents the change in state from when the call started to now. If the user is pinching the screen, it will represent the amount the distance between the user’s fingers has changed, for instance.

The velocity represents the speed with which the delta value is changing from one call to the next. In our pinch example, a small velocity means the user is very slowly changing the distance between their fingers.

This extra information makes continuous gestures a fantastic tool for making interactive apps. If the user is dragging an image across the screen, continuous gestures let the image follow their finger exactly. If the user is scaling the image, we can zoom in on it as they pinch. The feedback is instant and intuitive. The price of this wonderful tool is the extra code required to handle the gesture correctly, which we need to discuss before learning to use our new toys.

History-Sensitive Action Methods

A typical action for a continuous gesture might look something like this

- (IBAction)pinchGestureHandler:(UIPinchGestureRecognizer *)sender
{
    static CGPoint center;
    static CGSize initialSize;
    if (sender.state == UIGestureRecognizerStateBegan)
    {
        center = sender.view.center;
        initialSize = sender.view.frame.size;
    }
    sender.view.frame = CGRectMake(0,
                                   0,
                                   initialSize.width * sender.scale,
                                   initialSize.height * sender.scale);
    sender.view.center = center;
}

The first thing you might notice about this method is the use of a history-sensitive variable. Remember how this method is going to be called repeatedly over the course of the gesture? And remember how the delta value (scale, in this case) is sent along with each call? The scale is relative to the size when the gesture started, not the size at the last call. Meaning that if we increase the scale of the view by the value in sender.scale each time the method is called, the scale of the view will grow exponentially. In general, that is exactly not what we want, so we record what the view looked like when the gesture began, and increase the scale relative to that value each time.

To facilitate recording the initial value (among other things), continuous gestures include a state property to give the handler some indication of what the gesture is doing. For the most part, the only states a programmer is concerned with are UIGestureRecognizerStateBegan and UIGestureRecognizerStateEnded. Those are your opportunity to do any preliminary setup logic and any necessary takedown logic.

With that, let’s write some code.

Pinch

As usual, we begin by creating a new project in XCode. If you have been following along with previous tutorials, you can simply create a new UIViewController subclass in a preexisting project.

In InterfaceBuilder, add something to the view. Any GUI element will do, but I will be using a UIImageView. If you choose to use something else, be careful if it’s an editable component (like a UITextView) because they already have gestures attached. Your new gesture may interfere with the preexisting gestures, or vice versa.

To add a UIPinchGestureRecognizer, we simply drag a new Pinch Gesture Recognizer onto the UIImageView (or whatever component you chose for this tutorial). This will automatically set the Referencing Outlet Collections for the gesture, which makes it sensitive to touches in the image view only.

To create the action method, we select the pinch gesture recognizer in the Objects list in the left-hand pane of InterfaceBuilder.

 

 

Then, in the right-hand pane, we select the “Connection inspector” view (small circle with a right-facing arrow)

 

And just above and to the left of that, show the “Assistant Editor” (the “tuxedo” button)

 

 

 

Then click and drag from the small add connection circle into the header file (which should have appeared when you opened the Assistant Editor).

 

 

 

 

Name your method however you like, but try to be descriptive and consistent:

- (IBAction)pinchGestureHandler:(UIPinchGestureRecognizer *)sender;

If you did not select a type for the method, it will default to “id” for the parameter. This is okay, but you will have to cast sender to a UIPinchGestureRecognizer.

Now that we have a action method, we switch to the .m file and fill it with our code. In this case, we simply want to scale the image with the pinch gesture, like so:

static CGPoint center;
static CGSize initialSize;

if (sender.state == UIGestureRecognizerStateBegan)
{
    center = sender.view.center;
    initialSize = sender.view.frame.size;
}

// scale the image
sender.view.frame = CGRectMake(0,
                               0,
                               initialSize.width * sender.scale,
                               initialSize.height * sender.scale);
// recenter it with the new dimensions
sender.view.center = center;

Note that there are less verbose ways to scale an image, but we’re concerned with the gesture, not matrix theory.

Now build and run your app, and the image should scale as you pinch! (Note: if you are using the iOS simulator, you can pinch by holding “alt” and clicking)

Pan

Panning is accomplished almost exactly the same way as pinch. Grab a UIPanGestureRecognizer as before with the UIPinchGestureRecognizer and drag it onto the image in InterfaceBuilder. Create an action method for it (the part where you dragged the little circle into the header file), and fill the action method with the relevant code:

- (IBAction)panGestureHandler:(UIPanGestureRecognizer *)sender
{
    static CGPoint initialCenter;
    if (sender.state == UIGestureRecognizerStateBegan)
    {
        initialCenter = sender.view.center;
    }
    CGPoint translation = [sender translationInView:sender.view];
    sender.view.center = CGPointMake(initialCenter.x + translation.x,
                                     initialCenter.y + translation.y);
}

Build and run your app, and the image should scale as you pinch…and move as you drag!

Rotate

Much as I wish I could say that rotation was accomplished much the same way as pinch and pan, it’s not. The gesture recognizers are the same: Grab a UIRotationGestureRecognizer. Drag it onto the image. Make an action method. Good.

Trigonometry…

The rotation gesture recognizer yields its delta value in the form of an angle. In radians. For reasons that escape me, positive is clockwise and negative is counter-clockwise–running exactly counter to all that is good and holy in the world of angles. That’s okay, though, because we don’t care. We’re going to construct a transform out of the angle and let Cocoa deal with it:

sender.view.transform = CGAffineTransformMakeRotation(newRotation);
Unfortunately, in order to figure out what the new rotation is, we need to figure out what the old rotation was. That means we must calculate the arctangent of the view. I will save you the heartache, and just give you the answer:
initialRotation = atan2f(sender.view.transform.b, sender.view.transform.a);
Don’t worry about sender.view.transform right now. It’s the matrix theory we talked about earlier. For our purposes, it is the thing that rotates stuff for us.
Combining those lines and adding some standard gesture stuff, we get a complete handler:
- (IBAction)rotateGestureHandler:(UIRotationGestureRecognizer *)sender
{    
    static CGFloat initialRotation;
    if (sender.state == UIGestureRecognizerStateBegan)
    {
        initialRotation = atan2f(sender.view.transform.b, sender.view.transform.a);
    }
    CGFloat newRotation = initialRotation + sender.rotation;
    sender.view.transform = CGAffineTransformMakeRotation(newRotation);
}

Build and run your app, and the image should scale as you pinch…and move as you drag. and rotate as you…uh…rotate..

“But it doesn’t work!” you wail (assuming that you first rotated the image, and then tried to drag it around. Which you should do now, if you haven’t already.).

Nope. We’ve got a problem.

A little while ago we added this line to our app:

CGPoint translation = [sender translationInView:sender.view];

Innocent enough. We want the translation in the view. That’s what it does. Unfortunately, the view in question is the image. And the image just got rotated. And rotating a view really rotates the view. To the point that the coordinates themselves are rotated. If you turn the image on its side, “up” is going to be sideways. It’s a little mind-blowing, I know.

Take a moment to try and drag the thing in a straight line after it’s been rotated. When you’ve given up (or discovered the secret), we’ll fix our little bug.

Done?

We got lucky. There’s a saving grace. When we get call translationInView:, we get to choose the view in question. It does not have to be the view the gesture is attached to. It does not even have to be on-screen at the time.

Conveniently, we have another view directly behind the rotating image. The Superview (to the rescue). But it really doesn’t matter what view we use for this. Got a random button off to the side somewhere that’s not doing anything rotation related? Perfect. Use that. Invisible text view at the bottom of the screen? Just as good. Literally anything that provides a nice point-of-reference will work. Just change that translationInView: line to use anything other than our rotating image:

[sender translationInView:sender.view.superview]
And it’s fixed.

4 thoughts on “8. Pinch, Pan, & Rotate

  1. Hi,

    Thank you for this great tutorial. I sitll have a question. I made the pinch to scale on an image but I don’t want to be able to have a smaller scale than the original scale. How can I animate my image view to come back to the original scale when zoom out?

    Thank’s a lot!

    • You can find a tutorial covering animation here.

      In your handler for pinch, you can check if sender.state is “ended” and animate the image back to its original size if necessary.

  2. Hi, great post. It helped me solve a problem I’ve been having when re-zooming causing my image to center itself again before performing the zoom. Horrible UX that was driving me crazy! Thanks for such a detailed explanation and for sharing.

  3. I dont suppose you know how to fix the panning/rotation issue in swift?
    I cant seem to find any solid answers anywhere and am having trouble working it out by myself.

    thanks,
    Patrick

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s