Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!)
like image 769
PinkeshGjr Avatar asked Dec 29 '18 05:12

PinkeshGjr


1 Answers

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.

like image 198
m_katsifarakis Avatar answered Oct 06 '22 00:10

m_katsifarakis