Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pinch, Pan, and Rotate Text Simultaneously like Snapchat [SWIFT 3]

I'm trying to make a TextView that you can move around like snapchat does. I have made something similar to it, although when you try and scale while rotates, it tends to stretch horizontally infinitely and dragging can be a little buggy at times.

I have this:

func panGesture(pan: UIPanGestureRecognizer) {
    print("Being Dragged")
    if pan.state == .began {
        textViewOrigin = pan.location(in: textView)
    }else {
        let location = pan.location(in: view) // get pan location
        textView.frame.origin = CGPoint(x: location.x - textViewOrigin.x, y: location.y - textViewOrigin.y)
    }
}
func scaleGesture(_ gesture: UIPinchGestureRecognizer){
    print("Being Scaled")
    switch gesture.state{
    case.began:
        identity = textView.transform
    case.changed,.ended:
        textView.transform = identity.scaledBy(x: gesture.scale, y: gesture.scale)
    default:
        break
    }
}
func rotationGesture(sender: UIRotationGestureRecognizer){
    print("Being Rotated")
    textView.transform = textView.transform.rotated(by: sender.rotation)
    sender.rotation = 0
}

and I am trying to accomplish this: enter image description here

If someone could help alter or re-write my code that would be great, thanks in advance!

like image 392
Skiddswarmik Avatar asked Jul 30 '17 17:07

Skiddswarmik


2 Answers

By default, after one gesture recognizer on a view starts handling the gesture, other recognizers are ignored. In Swift, this behaviour can be controlled by overriding the method,

gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)^1,

to return true. The default implementation returns false.

To override the function just add your implementation, returning true, to your ViewController source code file. Here is some sample Swift code:

class ViewController: UIViewController,UIGestureRecognizerDelegate {

    @IBOutlet var textField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        //add pan gesture
        let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
        gestureRecognizer.delegate = self
        textField.addGestureRecognizer(gestureRecognizer)

        //Enable multiple touch and user interaction for textfield
        textField.isUserInteractionEnabled = true
        textField.isMultipleTouchEnabled = true

        //add pinch gesture
        let pinchGesture = UIPinchGestureRecognizer(target: self, action:#selector(pinchRecognized(pinch:)))
        pinchGesture.delegate = self
        textField.addGestureRecognizer(pinchGesture)

        //add rotate gesture.
        let rotate = UIRotationGestureRecognizer.init(target: self, action: #selector(handleRotate(recognizer:)))
        rotate.delegate = self
        textField.addGestureRecognizer(rotate)


    }

    func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
        if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {

            let translation = gestureRecognizer.translation(in: self.view)
            // note: 'view' is optional and need to be unwrapped
            gestureRecognizer.view!.center = CGPoint(x: gestureRecognizer.view!.center.x + translation.x, y: gestureRecognizer.view!.center.y + translation.y)
            gestureRecognizer.setTranslation(CGPoint.zero, in: self.view)
        }

    }

    func pinchRecognized(pinch: UIPinchGestureRecognizer) {

        if let view = pinch.view {
            view.transform = view.transform.scaledBy(x: pinch.scale, y: pinch.scale)
            pinch.scale = 1
        }
    }

    func handleRotate(recognizer : UIRotationGestureRecognizer) {
        if let view = recognizer.view {
            view.transform = view.transform.rotated(by: recognizer.rotation)
            recognizer.rotation = 0
        }
    }

    //MARK:- UIGestureRecognizerDelegate Methods
    func gestureRecognizer(_: UIGestureRecognizer,
                           shouldRecognizeSimultaneouslyWith shouldRecognizeSimultaneouslyWithGestureRecognizer:UIGestureRecognizer) -> Bool {
        return true
    }
}

Here's the crucial override in Objective-C^2:

-(BOOL)gestureRecognizer:(UIGestureRecognizer*)aR1 shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)aR2
{
    return YES;
}
like image 164
Nikhlesh Bagdiya Avatar answered Nov 15 '22 02:11

Nikhlesh Bagdiya


I write a class for easy to use.

Basic usage:

// define 
var snapGesture: SnapGesture?

// add gesture
self.snapGesture = SnapGesture(view: self.testView!)

// remove gesture
self.snapGesture = nil

Advanced usage, for scenario that the view receiving gesture is background view:

// add gesture
self.snapGesture = SnapGesture(transformView: self.testView!, gestureView: self.view)

// remove gesture
self.snapGesture = nil

class:

import UIKit

/*
 usage:

    add gesture:
        yourObjToStoreMe.snapGesture = SnapGesture(view: your_view)
    remove gesture:
        yourObjToStoreMe.snapGesture = nil
    disable gesture:
        yourObjToStoreMe.snapGesture.isGestureEnabled = false
    advanced usage:
        view to receive gesture(usually superview) is different from view to be transformed,
        thus you can zoom the view even if it is too small to be touched.
        yourObjToStoreMe.snapGesture = SnapGesture(transformView: your_view_to_transform, gestureView: your_view_to_recieve_gesture)

 */

class SnapGesture: NSObject, UIGestureRecognizerDelegate {

    // MARK: - init and deinit
    convenience init(view: UIView) {
        self.init(transformView: view, gestureView: view)
    }
    init(transformView: UIView, gestureView: UIView) {
        super.init()

        self.addGestures(v: gestureView)
        self.weakTransformView = transformView
    }
    deinit {
        self.cleanGesture()
    }

    // MARK: - private method
    private weak var weakGestureView: UIView?
    private weak var weakTransformView: UIView?

    private var panGesture: UIPanGestureRecognizer?
    private var pinchGesture: UIPinchGestureRecognizer?
    private var rotationGesture: UIRotationGestureRecognizer?

    private func addGestures(v: UIView) {

        panGesture = UIPanGestureRecognizer(target: self, action: #selector(panProcess(_:)))
        v.isUserInteractionEnabled = true
        panGesture?.delegate = self     // for simultaneous recog
        v.addGestureRecognizer(panGesture!)

        pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinchProcess(_:)))
        //view.isUserInteractionEnabled = true
        pinchGesture?.delegate = self   // for simultaneous recog
        v.addGestureRecognizer(pinchGesture!)

        rotationGesture = UIRotationGestureRecognizer(target: self, action: #selector(rotationProcess(_:)))
        rotationGesture?.delegate = self
        v.addGestureRecognizer(rotationGesture!)

        self.weakGestureView = v
    }

    private func cleanGesture() {
        if let view = self.weakGestureView {
            //for recognizer in view.gestureRecognizers ?? [] {
            //    view.removeGestureRecognizer(recognizer)
            //}
            if panGesture != nil {
                view.removeGestureRecognizer(panGesture!)
                panGesture = nil
            }
            if pinchGesture != nil {
                view.removeGestureRecognizer(pinchGesture!)
                pinchGesture = nil
            }
            if rotationGesture != nil {
                view.removeGestureRecognizer(rotationGesture!)
                rotationGesture = nil
            }
        }
        self.weakGestureView = nil
        self.weakTransformView = nil
    }




    // MARK: - API

    private func setView(view:UIView?) {
        self.setTransformView(view, gestgureView: view)
    }

    private func setTransformView(_ transformView: UIView?, gestgureView:UIView?) {
        self.cleanGesture()

        if let v = gestgureView  {
            self.addGestures(v: v)
        }
        self.weakTransformView = transformView
    }

    open func resetViewPosition() {
        UIView.animate(withDuration: 0.4) {
            self.weakTransformView?.transform = CGAffineTransform.identity
        }
    }

    open var isGestureEnabled = true

    // MARK: - gesture handle

    // location will jump when finger number change
    private var initPanFingerNumber:Int = 1
    private var isPanFingerNumberChangedInThisSession = false
    private var lastPanPoint:CGPoint = CGPoint(x: 0, y: 0)
    @objc func panProcess(_ recognizer:UIPanGestureRecognizer) {
        if isGestureEnabled {
            //guard let view = recognizer.view else { return }
            guard let view = self.weakTransformView else { return }

            // init
            if recognizer.state == .began {
                lastPanPoint = recognizer.location(in: view)
                initPanFingerNumber = recognizer.numberOfTouches
                isPanFingerNumberChangedInThisSession = false
            }

            // judge valid
            if recognizer.numberOfTouches != initPanFingerNumber {
                isPanFingerNumberChangedInThisSession = true
            }
            if isPanFingerNumberChangedInThisSession {
                return
            }

            // perform change
            let point = recognizer.location(in: view)
            view.transform = view.transform.translatedBy(x: point.x - lastPanPoint.x, y: point.y - lastPanPoint.y)
            lastPanPoint = recognizer.location(in: view)
        }
    }



    private var lastScale:CGFloat = 1.0
    private var lastPinchPoint:CGPoint = CGPoint(x: 0, y: 0)
    @objc func pinchProcess(_ recognizer:UIPinchGestureRecognizer) {
        if isGestureEnabled {
            guard let view = self.weakTransformView else { return }

            // init
            if recognizer.state == .began {
                lastScale = 1.0;
                lastPinchPoint = recognizer.location(in: view)
            }

            // judge valid
            if recognizer.numberOfTouches < 2 {
                lastPinchPoint = recognizer.location(in: view)
                return
            }

            // Scale
            let scale = 1.0 - (lastScale - recognizer.scale);
            view.transform = view.transform.scaledBy(x: scale, y: scale)
            lastScale = recognizer.scale;

            // Translate
            let point = recognizer.location(in: view)
            view.transform = view.transform.translatedBy(x: point.x - lastPinchPoint.x, y: point.y - lastPinchPoint.y)
            lastPinchPoint = recognizer.location(in: view)
        }
    }


    @objc func rotationProcess(_ recognizer: UIRotationGestureRecognizer) {
        if isGestureEnabled {
            guard let view = self.weakTransformView else { return }

            view.transform = view.transform.rotated(by: recognizer.rotation)
            recognizer.rotation = 0
        }
    }


    //MARK:- UIGestureRecognizerDelegate Methods
    func gestureRecognizer(_: UIGestureRecognizer,
                           shouldRecognizeSimultaneouslyWith shouldRecognizeSimultaneouslyWithGestureRecognizer:UIGestureRecognizer) -> Bool {
        return true
    }

}

Now, you reach this point, please continue. as you know, the gesture in superView may eat event of the subView, and in snapchat example, the gesture will eat event of the toolbar, we need prevent any gesture recognition from superview if we are touching the toolbar.

The idea is to add a pseduo customized gesture to the toolbar, so any gesture will be prevented to superview, and this pseduo gesture do nothing but deliver the gesture or event to subviews or its own view.

Here, I also write a class for easy to use.

usage:

   toolbarView.addGestureRecognizer(SnapBlockGestureRecognizer)

implementation:

import UIKit

class SnapBlockGestureRecognizer: UIGestureRecognizer {

    init() {
        //self.init(target: self, action: #selector(__dummyAction))
        super.init(target: nil, action: nil)

        self.addTarget(self, action: #selector(__dummyAction))
        self.cancelsTouchesInView = false
    }

    override init(target: Any?, action: Selector?) {
        super.init(target: target, action: action)

        self.cancelsTouchesInView = false
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        if self.state == .possible {
            self.state = .began
        }
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
        self.state = .recognized
    }

    override func canBePrevented(by preventingGestureRecognizer: UIGestureRecognizer) -> Bool {
        return self.isGestureRecognizerAllowed(gr:preventingGestureRecognizer)
    }


    override func canPrevent(_ preventedGestureRecognizer: UIGestureRecognizer) -> Bool {
        return !(self.isGestureRecognizerAllowed(gr: preventedGestureRecognizer))
    }

    override func shouldBeRequiredToFail(by otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return !(self.isGestureRecognizerAllowed(gr: otherGestureRecognizer))
    }

    override func shouldRequireFailure(of otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return false
    }

    func isGestureRecognizerAllowed(gr: UIGestureRecognizer) -> Bool {
        return gr.view!.isDescendant(of: self.view!)
    }

    @objc func __dummyAction() {
        // do nothing
        // print("dummyAction")
    }
}
like image 27
lbsweek Avatar answered Nov 15 '22 03:11

lbsweek