iOS Pinch Scale and Two Finger Rotate at same time

Solution 1:

Every time pinch: is called, you just compute the transform based on the pinch recognizer's scale. Every time pinchRotate: is called, you just compute the transform based on the rotation recognizer's rotation. You never combine the scale and the rotation into one transform.

Here's an approach. Give yourself one new instance variable, _activeRecognizers:

NSMutableSet *_activeRecognizers;

Initialize it in viewDidLoad:

_activeRecognizers = [NSMutableSet set];

Use one method as the action for both recognizers:

- (IBAction)handleGesture:(UIGestureRecognizer *)recognizer
{
    SMImage *selectedImage = [DataCenter sharedDataCenter].selectedImage;

    switch (recognizer.state) {
        case UIGestureRecognizerStateBegan:
            if (_activeRecognizers.count == 0)
                selectedImage.referenceTransform = selectedImage.transform;
            [_activeRecognizers addObject:recognizer];
            break;

        case UIGestureRecognizerStateEnded:
            selectedImage.referenceTransform = [self applyRecognizer:recognizer toTransform:selectedImage.referenceTransform];
            [_activeRecognizers removeObject:recognizer];
            break;

        case UIGestureRecognizerStateChanged: {
            CGAffineTransform transform = selectedImage.referenceTransform;
            for (UIGestureRecognizer *recognizer in _activeRecognizers)
                transform = [self applyRecognizer:recognizer toTransform:transform];
            selectedImage.transform = transform;
            break;
        }

        default:
            break;
    }
}

You'll need this helper method:

- (CGAffineTransform)applyRecognizer:(UIGestureRecognizer *)recognizer toTransform:(CGAffineTransform)transform
{
    if ([recognizer respondsToSelector:@selector(rotation)])
        return CGAffineTransformRotate(transform, [(UIRotationGestureRecognizer *)recognizer rotation]);
    else if ([recognizer respondsToSelector:@selector(scale)]) {
        CGFloat scale = [(UIPinchGestureRecognizer *)recognizer scale];
        return CGAffineTransformScale(transform, scale, scale);
    }
    else
        return transform;
}

This works if you're just allowing rotating and scaling. (I even tested it!)

If you want to add panning, use a separate action method and just adjust selectedImage.center. Trying to do panning with rotation and scaling using selectedImage.transform is much more complicated.

Solution 2:

Swift 3 with Pan, Rotate and Pinch

// MARK: - Gesturies

    func transformUsingRecognizer(_ recognizer: UIGestureRecognizer, transform: CGAffineTransform) -> CGAffineTransform {

        if let rotateRecognizer = recognizer as? UIRotationGestureRecognizer {
            return transform.rotated(by: rotateRecognizer.rotation)
        }

        if let pinchRecognizer = recognizer as? UIPinchGestureRecognizer {
            let scale = pinchRecognizer.scale
            return transform.scaledBy(x: scale, y: scale)
        }

        if let panRecognizer = recognizer as? UIPanGestureRecognizer {
            let deltaX = panRecognizer.translation(in: imageView).x
            let deltaY = panRecognizer.translation(in: imageView).y
            return transform.translatedBy(x: deltaX, y: deltaY)
        }

        return transform
    }

    var initialTransform: CGAffineTransform?

    var gestures = Set<UIGestureRecognizer>(minimumCapacity: 3)

    @IBAction func processTransform(_ sender: Any) {

        let gesture = sender as! UIGestureRecognizer

        switch gesture.state {

        case .began:
            if gestures.count == 0 {
                initialTransform = imageView.transform
            }
            gestures.insert(gesture)

        case .changed:
            if var initial = initialTransform {
                gestures.forEach({ (gesture) in
                    initial = transformUsingRecognizer(gesture, transform: initial)
                })
                imageView.transform = initial
            }

        case .ended:
            gestures.remove(gesture)

        default:
            break
        }
    }

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {

        return true
    }

Solution 3:

For this to happen you need to implement gesture delegate shouldRecognizeSimultaneouslyWithGestureRecognizer and put what gestures you would like to recognize simultaneously.

// ensure that the pinch and rotate gesture recognizers on a particular view can all recognize simultaneously
// prevent other gesture recognizers from recognizing simultaneously
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    // if the gesture recognizers's view isn't one of our views, don't allow simultaneous recognition
    if (gestureRecognizer.view != firstView && gestureRecognizer.view != secondView)
        return NO;

    // if the gesture recognizers are on different views, don't allow simultaneous recognition
    if (gestureRecognizer.view != otherGestureRecognizer.view)
        return NO;

    // if either of the gesture recognizers is the long press, don't allow simultaneous recognition
    if ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]] || [otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]])
        return NO;

    return YES;
}

This code needs to be modified to the view for which you want simultaneous gesture recognisers. The above code is what you need.

Solution 4:

This example does not use gesture recognizers, and directly computes the transformation matrix. It also properly handles one-to-two finger transitions.

class PincherView: UIView {

    override var bounds :CGRect {
        willSet(newBounds) {
            oldBounds = self.bounds
        } didSet {
            self.imageLayer.position = ┼self.bounds
            self._adjustScaleForBoundsChange()
        }
    }
    var oldBounds :CGRect

    var touch₁  :UITouch?
    var touch₂  :UITouch?
    var p₁      :CGPoint?  // point 1 in image coordiate system
    var p₂      :CGPoint?  // point 2 in image coordinate system
    var p₁ʹ     :CGPoint?  // point 1 in view coordinate system
    var p₂ʹ     :CGPoint?  // point 2 in view coordinate system

    var image   :UIImage? {
        didSet {self._reset()}
    }
    var imageLayer :CALayer
    var imageTransform :CGAffineTransform {
        didSet {
            self.backTransform = self.imageTransform.inverted()
            self.imageLayer.transform = CATransform3DMakeAffineTransform(self.imageTransform)
        }
    }
    var backTransform  :CGAffineTransform
    var solutionMatrix :HXMatrix?

    required init?(coder aDecoder: NSCoder) {
        self.oldBounds = CGRect.zero
        let layer = CALayer();
        self.imageLayer = layer
        self.imageTransform = CGAffineTransform.identity
        self.backTransform = CGAffineTransform.identity

        super.init(coder: aDecoder)

        self.oldBounds = self.bounds
        self.isMultipleTouchEnabled = true
        self.layer.addSublayer(layer)
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {
            let pʹ = touch.location(in: self).applying(self._backNormalizeTransform())
            let p = pʹ.applying(self.backTransform)
            if self.touch₁ == nil {
                self.touch₁ = touch
                self.p₁ʹ = pʹ
                self.p₁ = p
            } else if self.touch₂ == nil {
                self.touch₂ = touch
                self.p₂ʹ = pʹ
                self.p₂ = p
            }
        }
        self.solutionMatrix = self._computeSolutionMatrix()
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {
            let pʹ = touch.location(in: self).applying(self._backNormalizeTransform())
            if self.touch₁ == touch {
                self.p₁ʹ = pʹ
            } else if self.touch₂ == touch {
                self.p₂ʹ = pʹ
            }
        }

        CATransaction.begin()
        CATransaction.setValue(true, forKey:kCATransactionDisableActions)
        // Whether you're using 1 finger or 2 fingers
        if let q₁ʹ = self.p₁ʹ, let q₂ʹ = self.p₂ʹ {
            self.imageTransform = self._computeTransform(q₁ʹ, q₂ʹ)
        } else if let q₁ʹ = (self.p₁ʹ != nil ? self.p₁ʹ : self.p₂ʹ) {
            self.imageTransform = self._computeTransform(q₁ʹ, CGPoint(x:q₁ʹ.x + 10, y:q₁ʹ.y + 10))
        }
        CATransaction.commit()
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {
            if self.touch₁ == touch {
                self.touch₁ = nil
                self.p₁ = nil
                self.p₁ʹ = nil
            } else if self.touch₂ == touch {
                self.touch₂ = nil
                self.p₂ = nil
                self.p₂ʹ = nil
            }
        }
        self.solutionMatrix = self._computeSolutionMatrix()
    }

    //MARK: Private Methods

    private func _reset() {
        guard
            let image = self.image,
            let cgimage = image.cgImage else {
                return
        }

        let r = CGRect(x:0, y:0, width:cgimage.width, height:cgimage.height)
        imageLayer.contents = cgimage;
        imageLayer.bounds = r
        imageLayer.position = ┼self.bounds
        self.imageTransform = self._initialTransform()
    }

    private func _normalizeTransform() -> CGAffineTransform {
        let center = ┼self.bounds
        return CGAffineTransform(translationX: center.x, y: center.y)
    }

    private func _backNormalizeTransform() -> CGAffineTransform {
        return self._normalizeTransform().inverted();
    }

    private func _initialTransform() -> CGAffineTransform {
        guard let image = self.image, let cgimage = image.cgImage else {
            return CGAffineTransform.identity;
        }
        let r = CGRect(x:0, y:0, width:cgimage.width, height:cgimage.height)
        let s = r.scaleIn(rect: self.bounds)
        return CGAffineTransform(scaleX: s, y: s)
    }

    private func _adjustScaleForBoundsChange() {
        guard let image = self.image, let cgimage = image.cgImage else {
            return
        }
        let r = CGRect(x:0, y:0, width:cgimage.width, height:cgimage.height)
        let oldIdeal = r.scaleAndCenterIn(rect: self.oldBounds)
        let newIdeal = r.scaleAndCenterIn(rect: self.bounds)
        let s = newIdeal.height / oldIdeal.height
        self.imageTransform = self.imageTransform.scaledBy(x: s, y: s)
    }

    private func _computeSolutionMatrix() -> HXMatrix? {
        if let q₁ = self.p₁, let q₂ = self.p₂ {
            return _computeSolutionMatrix(q₁, q₂)
        } else if let q₁ = self.p₁, let q₁ʹ = self.p₁ʹ {
            let q₂ = CGPoint(x: q₁ʹ.x + 10, y: q₁ʹ.y + 10).applying(self.backTransform)
            return _computeSolutionMatrix(q₁, q₂)
        } else if let q₂ = self.p₂, let q₂ʹ = self.p₂ʹ {
            let q₁ = CGPoint(x: q₂ʹ.x + 10, y: q₂ʹ.y + 10).applying(self.backTransform)
            return _computeSolutionMatrix(q₂, q₁)
        }
        return nil
    }

    private func _computeSolutionMatrix(_ q₁:CGPoint, _ q₂:CGPoint) -> HXMatrix {
        let x₁ = Double(q₁.x)
        let y₁ = Double(q₁.y)
        let x₂ = Double(q₂.x)
        let y₂ = Double(q₂.y)
        let A = HXMatrix(rows: 4, columns: 4, values:[
            x₁, -y₁, 1, 0,
            y₁,  x₁, 0, 1,
            x₂, -y₂, 1, 0,
            y₂,  x₂, 0, 1
        ])
        return A.inverse()
    }

    private func _computeTransform(_ q₁ʹ:CGPoint, _ q₂ʹ:CGPoint) -> CGAffineTransform {
        guard let solutionMatrix = self.solutionMatrix else {
            return CGAffineTransform.identity
        }

        let B = HXMatrix(rows: 4, columns: 1, values: [
            Double(q₁ʹ.x),
            Double(q₁ʹ.y),
            Double(q₂ʹ.x),
            Double(q₂ʹ.y)
            ])
        let C = solutionMatrix ⋅ B

        let  U = CGFloat(C[0,0])
        let  V = CGFloat(C[1,0])
        let tx = CGFloat(C[2,0])
        let ty = CGFloat(C[3,0])

        var  t :CGAffineTransform = CGAffineTransform.identity
        t.a  =  U; t.b  = V
        t.c  = -V; t.d  = U
        t.tx = tx; t.ty = ty

        return t
    }
}