Simultaneously recognising UIPanGestureRecognizer, UIRotationGestureRecognizer, and UIPinchGestureRecognizer
Give this a try -- seems to work well for simultaneous Pan / Rotate / Scale:
class PinchPanRotateViewController: UIViewController, UIGestureRecognizerDelegate {
let testView: UILabel = {
let v = UILabel()
v.text = "TEST"
v.textAlignment = .center
v.textColor = .yellow
v.layer.cornerRadius = 4.0
v.layer.borderWidth = 4.0
v.layer.borderColor = UIColor.red.cgColor
v.layer.masksToBounds = true
//Enable multiple touch and user interaction
v.isUserInteractionEnabled = true
v.isMultipleTouchEnabled = true
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .blue
view.addSubview(testView)
testView.frame = CGRect(x: 0, y: 0, width: 240, height: 180)
testView.center = view.center
//add pan gesture
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
gestureRecognizer.delegate = self
testView.addGestureRecognizer(gestureRecognizer)
//add pinch gesture
let pinchGesture = UIPinchGestureRecognizer(target: self, action:#selector(handlePinch(_:)))
pinchGesture.delegate = self
testView.addGestureRecognizer(pinchGesture)
//add rotate gesture.
let rotate = UIRotationGestureRecognizer.init(target: self, action: #selector(handleRotate(_:)))
rotate.delegate = self
testView.addGestureRecognizer(rotate)
}
@objc func handlePan(_ pan: UIPanGestureRecognizer) {
if pan.state == .began || pan.state == .changed {
guard let v = pan.view else { return }
let translation = pan.translation(in: self.view)
v.center = CGPoint(x: v.center.x + translation.x, y: v.center.y + translation.y)
pan.setTranslation(CGPoint.zero, in: self.view)
}
}
@objc func handlePinch(_ pinch: UIPinchGestureRecognizer) {
guard let v = pinch.view else { return }
v.transform = v.transform.scaledBy(x: pinch.scale, y: pinch.scale)
pinch.scale = 1
}
@objc func handleRotate(_ rotate: UIRotationGestureRecognizer) {
guard let v = rotate.view else { return }
v.transform = v.transform.rotated(by: rotate.rotation)
rotate.rotation = 0
}
func gestureRecognizer(_: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith shouldRecognizeSimultaneouslyWithGestureRecognizer:UIGestureRecognizer) -> Bool {
return true
}
}
A way to add Instagram like layout guide with UIPinchGestureRecognizer UIRotationGestureRecognizer & UIPanGestureRecognizer?
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.
Simultaneous gesture recognition for specific gestures
Make sure your class implements UIGestureRecognizerDelegate
class YourViewController: UIViewController, UIGestureRecognizerDelegate ...
Set the gesture's delegate
to self
yourGesture.delegate = self
Add delegate function to your class
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if (gestureRecognizer is UIPanGestureRecognizer || gestureRecognizer is UIRotationGestureRecognizer) {
return true
} else {
return false
}
}
Handling Multiple GestureRecognizers
The UIGestureRecognizerDelegate
has a special function managing simultaneous recognition of several gestures on the same object, that will do the trick.
1) Set your UIViewController
to conform UIGestureRecognizerDelegate
2) Implement the following function:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if (gestureRecognizer == mainScene.panRecognizer || gestureRecognizer == mainScene.pinchRecognizer) && otherGestureRecognizer == mainScene.tapRecognizer {
return true
}
return false
}
In this particular example we allow the tap gesture to get triggered simultaneously with panning and pinching.
3) Then just assign the delegates to the pan and pinch gesture recognizers:
override func viewDidLoad() {
// your code...
// Set gesture recognizers delegates
mainScene.panRecognizer.delegate = self
mainScene.pinchRecognizer.delegate = self
}
Handle two Gesture Recognizer simultaneously
With the help of Mitesh Mistri I got it working. One more thing that was missing was a function to disable the panGR
inside the tableView
because otherwise the user wouldnt be able to scroll. These are the two function that made it work:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if self.theTableView.view.bounds.contains(touch.location(in: self.theTableView.view)) {
return false
}
return true
}
Two UIGestureRecognizers
Since you want to kill the secondary gesture only when the primary gesture has ended or cancelled, do this in the gesture handler of the primary gesture.
- (void)handleGesture:(UIGestureRecognizer*)gesture {
...
if ( gesture.state == UIGestureRecognizerStateEnded || gesture.state == UIGestureRecognizerCancelled ) {
secondaryGesture.enabled = NO;
secondaryGesture.enabled = YES;
}
}
This seems to be the only way you can cancel a gesture.
You can use
requireGestureRecognizerToFail:
to declare a dependency.[secondaryGesture requireGestureRecognizerToFail:primaryGesture];
This will kill the secondary gesture on successful identification of the primary gesture. There is no such tool provided if the primary gesture is cancelled. You can probably flip the enabled
flag of the secondary gesture to NO
and YES
in the gesture handler of the primary gesture on UIGestureRecognizerStateCancelled
but that doesn't seem elegant.
Pinch, Pan, and Rotate Text Simultaneously like Snapchat [SWIFT 3]
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;
}
Related Topics
How to Auto Call an @Ibaction Function
Exc_Bad_Access in Parent Class Init() with Xcode 10.2
How to Use a Generic Class Without the Type Argument in Swift
Custom Uitoolbar Too Close to the Home Indicator on iPhone X
Swift How to "Pass by Value" of a Object
iOS 10 Issue: Uiscrollview Not Scrolling, Even When Contentsize Is Set
Make Skspritenode Subclass Using Swift
How to Create a Rounded Rectangle Label in Xcode 7 and Swift 2
Cannot Set Color of Button's Label Inside Menu in Swiftui
How to Make a Uiimage Scrollable Inside a Uiscrollview
My Uiviews Muck-Up When I Combine Uipangesturerecognizer and Autolayout
iOS with Parse. Pfuser.Currentuser() Not Getting Cached. Returns Nil After App Restart
How to Run My Performance Tests More Than Ten Times
Receipt Validation on iOS In-App-Purchase Returns Multiple Transaction