Episode #47

Gesture Recognizers

29 minutes
Published on January 3, 2013

This video is only available to subscribers. Get access to this video and 584 others.

Detecting touches used to be a chore. Thanks to the UIGestureRecognizer family of classes, detecting touches & gestures is a breeze. In this episode we implement a Photo Table where you can add photos, move them around, as well as pinch & rotate.

Episode Links

Adding a Photo with a Double Tap

UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
                                                                   action:@selector(onViewDoubleTap:)]; 
tapRecognizer.numberOfTapsRequired = 2;
[self.view addGestureRecognizer:tapRecognizer];
- (void)onViewDoubleTap:(UITapGestureRecognizer *)tap {
    CGPoint touchCenter = [tap locationInView:self.view];

    UIImageView *imageView = [[UIImageView alloc] initWithImage:[self nextImage]];

    CGRect imageViewFrame = CGRectMake(0, 0, 200, 200);
    imageViewFrame.origin = CGPointMake(touchCenter.x - imageViewFrame.size.width / 2.0f, touchCenter.y - imageViewFrame.size.height / 2.0f);
    imageView.frame = imageViewFrame;

    imageView.layer.borderWidth = 4;
    imageView.layer.borderColor = [[UIColor whiteColor] CGColor];
    imageView.layer.shadowColor = [[UIColor blackColor] CGColor];
    imageView.layer.shadowOffset = CGSizeMake(0, 1);

    CGMutablePathRef shadowPath = CGPathCreateMutable();
    CGAffineTransform transform = imageView.transform;
    CGPathAddRect(shadowPath, &transform, imageView.bounds);

    imageView.layer.shadowOpacity = 0.5;
    imageView.layer.shadowPath = shadowPath;

    CGPathRelease(shadowPath);

    imageView.alpha = 0.75;
    imageView.transform = CGAffineTransformMakeScale(1.25, 1.25);

    CGFloat angle = DEGREES_TO_RADIANS((arc4random() % 10) - 5);
    imageView.transform = CGAffineTransformRotate(imageView.transform, angle);

    [self.view addSubview:imageView];

    [UIView animateWithDuration:0.25 animations:^{
        imageView.alpha = 1;
        imageView.transform = CGAffineTransformIdentity;
    }];

    [self addGestureRecognizersToView:imageView];
}

- (void)addGestureRecognizersToView:(UIView *)view {
    view.userInteractionEnabled = YES;

    //...
}

Moving pictures around

We just need to add a pan recognizer to each image view. In the callback, we'll just offset the center point of the image by the location of the touch.

- (void)addGestureRecognizersToView:(UIView *)view {
    //...

    UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self
                                                                                    action:@selector(onPan:)];
    panRecognizer.delegate = self;
    [view addGestureRecognizer:panRecognizer];

    //...
}

- (void)onPan:(UIPanGestureRecognizer *)pan {
    CGPoint offset = [pan translationInView:self.view];
    CGPoint center = pan.view.center;
    center.x += offset.x;
    center.y += offset.y;
    pan.view.center = center;

    [pan setTranslation:CGPointZero inView:self.view];
}

Note that once we have accounted for the translation amount, we have to "zero it out" so that it won't affect the next callbacks. If we do not, the translation amount will be cumulative and the view will eventually slide out from under your finger.

Pinching to scale the image

- (void)addGestureRecognizersToView:(UIView *)view {
    //...

    UIPinchGestureRecognizer *pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self
                                                                                          action:@selector(onPinch:)];
    pinchRecognizer.delegate = self;
    [view addGestureRecognizer:pinchRecognizer];

    //...
}

- (void)onPinch:(UIPinchGestureRecognizer *)pinch {
    CGFloat scale = [pinch scale];
    UIView *view = pinch.view;
    view.transform = CGAffineTransformScale(view.transform, scale, scale);
    [pinch setScale:1];
}

Here, we again "zero out" the scale by setting it to one after we have used the scale to alter the view's transform.

Rotating the images

Rotating is similar to pinching to resize...

- (void)addGestureRecognizersToView:(UIView *)view {
    //...

    UIRotationGestureRecognizer *rotationRecognizer = [[UIRotationGestureRecognizer alloc] initWithTarget:self
                                                                                                   action:@selector(onRotate:)];
    rotationRecognizer.delegate = self;
    [view addGestureRecognizer:rotationRecognizer];

    //...
}

- (void)onRotate:(UIRotationGestureRecognizer *)rotation {
    CGFloat angle = [rotation rotation];
    rotation.view.transform = CGAffineTransformRotate(rotation.view.transform, angle);
    [rotation setRotation:0];
}

Allowing simultaneous gestures

By default, while one recognizer is active, the others won't work. To get around this, you have to mark the class as conforming to the UIGestureRecognizerDelegate protocol and set each gesture recognizers delegate property to self.

The you can implement this method:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer 
    shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    return YES;
}

Now you can pinch, rotate & pan at the same time! Try panning multiple images at once as well.