19

I've implemented the UIPinchGestureRecognizer on a UIImageView in my app, however no matter where I pinch on the image, it seems to zoom into the same spot. Does anyone know how I can make it zoom in to where a user actually "pinches"? See code below.

ViewController.m

 - (IBAction)scaleImage:(UIPinchGestureRecognizer *)recognizer {

   recognizer.view.transform = CGAffineTransformScale(recognizer.view.transform, recognizer.scale, recognizer.scale);
   recognizer.scale = 1; 

 }

 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
{
    BOOL shouldReceiveTouch = YES;

    if (gestureRecognizer == tap) {
        shouldReceiveTouch = (touch.view == featureImage);
    }
    return shouldReceiveTouch;
}
Brittany
  • 1,359
  • 4
  • 24
  • 63

5 Answers5

39

A scale transform leaves the origin (0, 0) untouched. So to scale a view around a particular point, you must first translate that point to the origin, then apply the scale, then translate back.

- (IBAction)pinchGestureDidFire:(UIPinchGestureRecognizer *)pinch {

First, we get the view being pinched.

    UIView *pinchView = pinch.view;

To compute the center of the pinch, we'll need the midpoint of the view's bounds, so we get the bounds too:

    CGRect bounds = pinchView.bounds;

The center is based on the centroid of the pinch's touches, which we get this way:

    CGPoint pinchCenter = [pinch locationInView:pinchView];

But we actually need the pinch offset relative to the center of the view, because the view's transform is relative to the center of the view by default. (You can change this by changing the view's layer.anchorPoint.)

    pinchCenter.x -= CGRectGetMidX(bounds);
    pinchCenter.y -= CGRectGetMidY(bounds);

Now we can update the view's transform. First we get its current transform:

    CGAffineTransform transform = pinchView.transform;

Then we update it to translate the pinch center to the origin:

    transform = CGAffineTransformTranslate(transform, pinchCenter.x, pinchCenter.y);

Now we can apply the scale:

    CGFloat scale = pinch.scale;
    transform = CGAffineTransformScale(transform, scale, scale);

Then we translate the view back:

    transform = CGAffineTransformTranslate(transform, -pinchCenter.x, -pinchCenter.y);

Now we can update the view with the modified transform:

    pinchView.transform = transform;

Finally, we reset the gesture recognizer's scale, since we've applied the current scale:

    pinch.scale = 1.0;
}

Demo:

pinch scale

Note that in the simulator, you can hold option (alt) for a pinch gesture. Holding shift (while holding option) moves the two touches together.

Here's the code all together for copy/paste:

- (IBAction)pinchGestureDidFire:(UIPinchGestureRecognizer *)pinch {
    UIView *pinchView = pinch.view;
    CGRect bounds = pinchView.bounds;
    CGPoint pinchCenter = [pinch locationInView:pinchView];
    pinchCenter.x -= CGRectGetMidX(bounds);
    pinchCenter.y -= CGRectGetMidY(bounds);
    CGAffineTransform transform = pinchView.transform;
    transform = CGAffineTransformTranslate(transform, pinchCenter.x, pinchCenter.y);
    CGFloat scale = pinch.scale;
    transform = CGAffineTransformScale(transform, scale, scale);
    transform = CGAffineTransformTranslate(transform, -pinchCenter.x, -pinchCenter.y);
    pinchView.transform = transform;
    pinch.scale = 1.0;
}
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • This doesn't seem to work for me (though it seems like it should)? I implemented the above, and I'm still only able to zoom in to one spot on the image. – Brittany Sep 16 '14 at 15:02
  • 1
    Are you sure you connected the gesture recognizer to the new method and didn't leave it connected to the old method? Did you put an `NSLog` or a breakpoint in the new method to ensure that it's called? – rob mayoff Sep 16 '14 at 17:01
  • is it possible to apply not proportional zoom? i mean apply x and y changes separately? – Siarhei Feb 23 '17 at 15:46
  • It is not directly possible with `UIPinchGestureRecognizer`, because the recognizer only reports a single `scale` factor. You would need to look at the individual touches in the gesture, track their movements on each update, and compute the individual X and Y scale factors yourself. – rob mayoff Feb 23 '17 at 20:32
  • hey @robmayoff, what if I moved the picture in the middle of zoom, and then zoom again, would the x and y be ruined? And another question, how to limit the minimum zoom in bound? – bolizhou Jul 15 '17 at 09:51
  • If anyone is having @Brittany's problem, this answer wasn't working for me because I was updating a label's text in the same method. I don't know why this was. – olivaresF Feb 19 '18 at 01:48
  • This is awesome / working / the best answer! Thanks Rob! – bojan Oct 21 '19 at 19:41
22

Swift implementation of rob's answer:

@objc private func pinchHandler(gesture: UIPinchGestureRecognizer) {
    if let view = gesture.view {

        switch gesture.state {
        case .changed:
            let pinchCenter = CGPoint(x: gesture.location(in: view).x - view.bounds.midX,
                                      y: gesture.location(in: view).y - view.bounds.midY)
            let transform = view.transform.translatedBy(x: pinchCenter.x, y: pinchCenter.y)
                                            .scaledBy(x: gesture.scale, y: gesture.scale)
                                            .translatedBy(x: -pinchCenter.x, y: -pinchCenter.y)
            view.transform = transform
            gesture.scale = 1
        case .ended:
            // Nice animation to scale down when releasing the pinch.
            // OPTIONAL
            UIView.animate(withDuration: 0.2, animations: {
                view.transform = CGAffineTransform.identity
            })
        default:
            return
        }


    }
}
Bruno Paulino
  • 5,611
  • 1
  • 41
  • 40
3

Rob Mayoff's (@robmayoff) answer is truly inspiring and I wish I had found it earlier. But I actually solved this by changing the anchor point rather than using a transform translation. Effectively it has the same result.

I have not seen this solution on SO (yet...) and figured this was a good place for it.

Transforms work relative to the view layer anchor point. But you can't just set the position point to the pinch center point if the view has been transformed. So here's a method to set the layer anchor point AND correctly convert the position point as well:

// change the anchor point for the view layer
- (void)setAnchorPoint:(CGPoint)anchorPoint forView:(UIView *)view {
    // sanity check - x and y MUST be between 0 and 1
    if (anchorPoint.x < 0 || anchorPoint.x > 1 ||
        anchorPoint.y < 0 || anchorPoint.y > 1) {
        return;
    }

    CGPoint newPoint = CGPointMake(view.bounds.size.width * anchorPoint.x,
                                   view.bounds.size.height * anchorPoint.y);
    CGPoint oldPoint = CGPointMake(view.bounds.size.width * view.layer.anchorPoint.x,
                                   view.bounds.size.height * view.layer.anchorPoint.y);

    newPoint = CGPointApplyAffineTransform(newPoint, view.transform);
    oldPoint = CGPointApplyAffineTransform(oldPoint, view.transform);

    CGPoint position = view.layer.position;

    position.x -= oldPoint.x;
    position.x += newPoint.x;

    position.y -= oldPoint.y;
    position.y += newPoint.y;

    view.layer.position = position;
    view.layer.anchorPoint = anchorPoint;
}

And here's the handlePinch method that uses the setAnchorPoint method to set the layer anchor point to the center point of the pinch touch points:

- (void)handlePinch:(UIPinchGestureRecognizer *)recognizer {
    // make these static so they can be used across gesture states
    static CGAffineTransform initialTransform;
    static CGPoint initialAnchor;

    if (recognizer.state == UIGestureRecognizerStateBegan) {
        // save these for later states
        initialTransform = recognizer.view.transform;
        initialAnchor = recognizer.view.layer.anchorPoint;

        // get the center point of the pinch
        CGPoint touch = [recognizer locationInView:recognizer.view];

        // anchor point is relative to the view bounds:  0 ... up to 1.0, for both x and y
        CGFloat anchorX = touch.x / recognizer.view.bounds.size.width;
        CGFloat anchorY = touch.y / recognizer.view.bounds.size.height;

        // set the layer anchor point AND position, to where the view was initially pinched
        [self setAnchorPoint:CGPointMake(anchorX,anchorY) forView:recognizer.view];

    } else if (recognizer.state == UIGestureRecognizerStateChanged) {
        // perform the pinch zoom
        recognizer.view.transform = CGAffineTransformScale(initialTransform,recognizer.scale,recognizer.scale);

    } else if (recognizer.state == UIGestureRecognizerStateEnded) {
        // reset the scale when it's done
        recognizer.scale = 1;

        // restore the original anchor point
        [self setAnchorPoint:initialAnchor forView:recognizer.view];
    }
}
ByteSlinger
  • 1,439
  • 1
  • 17
  • 28
1
ViewController.h

@property (strong, nonatomic) IBOutlet UIImageView *img;

ViewController.m

- (void)viewDidLoad

 {

    [super viewDidLoad];

    UIPinchGestureRecognizer *pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchDetected:)];

    [_img addGestureRecognizer:pinchRecognizer];

    // Do any additional setup after loading the view, typically from a nib.

}

- (void)pinchDetected:(UIPinchGestureRecognizer *)pinchRecognizer

{

    CGFloat scale = pinchRecognizer.scale;

    _img.transform = CGAffineTransformScale(self.img.transform, scale, scale);

    pinchRecognizer.scale = 1.0;
}
ketan
  • 19,129
  • 42
  • 60
  • 98
0

You could simply add a scrollView and in the scrollView add a imageView as below,

![ImageView on top of scrollView][1]

Some issue with image upload so check image at this link

Then, you could create a IBOutlet property of scrollView and imageView ,and connect it respectively. Later, add these lines in your viewDidAppear method,

_scrollView.maximumZoomScale = 10.0;        //Maximum Zoom
_scrollView.minimumZoomScale = minimumScale;   //Minimum Zoom

Also add below method,

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{
   return _imageView;
}

So now you would get the zoom in/out effects and maximum/minimum zoom limit that u set. Lesser code along with achieving ur goal.

nikhil84
  • 3,235
  • 4
  • 22
  • 43
  • I would do this, but I don't like the idea of being able to see the scroll bar on the zoomed in image :/ – Brittany Sep 16 '14 at 15:06
  • idea of being able to see scroll bar is to let user know that he could move around to see the image(as in zoomed case he would be seeing art of full image) – nikhil84 Sep 17 '14 at 04:30
  • 1
    You can disable the scroll bars from the Interface Build by unchecking the "Show Horizontal / Vertical Indicator" options. It can even be done programmatically. – Abdurrahman Mubeen Ali Sep 19 '17 at 14:21