iosswiftinstagramuigesturerecognizer

A way to add Instagram like layout guide with UIPinchGestureRecognizer UIRotationGestureRecognizer & UIPanGestureRecognizer?


I have used UIPinchGestureRecognizer UIPanGestureRecognizer & UIRotationGestureRecognizer with UILabel to achieve Instagram like zoom and drag functionality. Now I would like to show layout guide like when UILabel is dragged in center it should show layout guide like below example. It should also display layout guide when you rotate UILabel.

What is the best and accurate possible way to achieve this functionality?

This is what I already have

(Image taken from this question by @Skiddswarmik)

Here is code I have for simple drag and zoom functionality (taken from this answer by @lbsweek)

SnapGesture 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
    }

}

Add Gesture in UILabel

// define 
var snapGesture: SnapGesture?

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

Solution

  • Below you will find an updated version of your class that should do what you describe.

    Most of the updated code is located at the last section (Guides) near the end, but I have updated your UIGestureRecognizer actions a bit as well as your main init method.

    Features:

    - A vertical guide for centering a view's position horizontally.

    - A horizontal guide for centering a view's rotation at 0 degrees.

    - Position and rotation snapping to guides with tolerance values (snapToleranceDistance and snapToleranceAngle properties).

    - Animated appearance / disappearance of guides (animateGuides and guideAnimationDuration properties).

    - Guide views that can be changed per use case (movementGuideView and rotationGuideView properties)

    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
    
            guard let transformView = self.weakTransformView, let superview = transformView.superview else {
                return
            }
    
            // This is required in order to be able to snap the view to center later on,
            // using the `tx` property of its transform.
            transformView.center = superview.center
        }
        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) {
            guard isGestureEnabled, 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 {
                hideGuidesOnGestureEnd(recognizer)
                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)
    
            updateMovementGuide()
            hideGuidesOnGestureEnd(recognizer)
        }
    
        private var lastScale:CGFloat = 1.0
        private var lastPinchPoint:CGPoint = CGPoint(x: 0, y: 0)
        @objc func pinchProcess(_ recognizer:UIPinchGestureRecognizer) {
            guard isGestureEnabled, 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)
                hideGuidesOnGestureEnd(recognizer)
                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)
    
            updateMovementGuide()
            hideGuidesOnGestureEnd(recognizer)
        }
    
    
        @objc func rotationProcess(_ recognizer: UIRotationGestureRecognizer) {
            guard isGestureEnabled, let view = self.weakTransformView else { return }
    
            view.transform = view.transform.rotated(by: recognizer.rotation)
            recognizer.rotation = 0
            updateRotationGuide()
            hideGuidesOnGestureEnd(recognizer)
        }
    
        func hideGuidesOnGestureEnd(_ recognizer: UIGestureRecognizer) {
            if recognizer.state == .ended {
                showMovementGuide(false)
                showRotationGuide(false)
            }
        }
    
        // MARK:- UIGestureRecognizerDelegate Methods
        func gestureRecognizer(_: UIGestureRecognizer,
                               shouldRecognizeSimultaneouslyWith shouldRecognizeSimultaneouslyWithGestureRecognizer:UIGestureRecognizer) -> Bool {
            return true
        }
    
        // MARK:- Guides
    
        var animateGuides = true
        var guideAnimationDuration: TimeInterval = 0.3
    
        var snapToleranceDistance: CGFloat = 5 // pts
        var snapToleranceAngle: CGFloat = 1    // degrees
                            * CGFloat.pi / 180 // (converted to radians)
    
        var movementGuideView: UIView = {
            let view = UIView()
            view.backgroundColor = UIColor.blue
            return view
        } ()
    
        var rotationGuideView: UIView = {
            let view = UIView()
            view.backgroundColor = UIColor.red
            return view
        } ()
    
        // MARK: Movement guide and snap
    
        func updateMovementGuide() {
            guard let transformView = weakTransformView, let superview = transformView.superview else {
                return
            }
    
            let transformX = transformView.frame.midX
            let superX = superview.bounds.midX
    
            if transformX - snapToleranceDistance < superX && transformX + snapToleranceDistance > superX {
                transformView.transform.tx = 0
                showMovementGuide(true)
            } else {
                showMovementGuide(false)
            }
    
            updateGuideFrames()
        }
    
        var isShowingMovementGuide = false
    
        func showMovementGuide(_ shouldShow: Bool) {
            guard isShowingMovementGuide != shouldShow,
                let transformView = weakTransformView,
                let superview = transformView.superview
                else { return }
    
            superview.insertSubview(movementGuideView, belowSubview: transformView)
            movementGuideView.frame = CGRect(
                x: superview.frame.midX,
                y: 0,
                width: 1,
                height: superview.frame.size.height
            )
    
            let duration = animateGuides ? guideAnimationDuration : 0
            isShowingMovementGuide = shouldShow
            UIView.animate(withDuration: duration) { [weak self] in
                self?.movementGuideView.alpha = shouldShow ? 1 : 0
            }
        }
    
        // MARK: Rotation guide and snap
    
        func updateRotationGuide() {
            guard let transformView = weakTransformView else {
                return
            }
    
            let angle = atan2(transformView.transform.b, transformView.transform.a)
            if angle > -snapToleranceAngle && angle < snapToleranceAngle {
                transformView.transform = transformView.transform.rotated(by: angle * -1)
                showRotationGuide(true)
            } else {
                showRotationGuide(false)
            }
        }
    
        var isShowingRotationGuide = false
    
        func showRotationGuide(_ shouldShow: Bool) {
            guard isShowingRotationGuide != shouldShow,
                let transformView = weakTransformView,
                let superview = transformView.superview
                else { return }
    
            superview.insertSubview(rotationGuideView, belowSubview: transformView)
    
            let duration = animateGuides ? guideAnimationDuration : 0
            isShowingRotationGuide = shouldShow
            UIView.animate(withDuration: duration) { [weak self] in
                self?.rotationGuideView.alpha = shouldShow ? 1 : 0
            }
        }
    
        func updateGuideFrames() {
            guard let transformView = weakTransformView,
                let superview = transformView.superview
                else { return }
    
            rotationGuideView.frame = CGRect(
                x: 0,
                y: transformView.frame.midY,
                width: superview.frame.size.width,
                height: 1
            )
        }
    }
    

    For anyone interested, here's a test project using this class.