UIPanGestureRecognizer - Only vertical or horizontal
I have a view that has a UIPanGestureRecognizer
to drag the view vertically. So in the recognizer callback, I only update the y-coordinate to move it. The superview of this view, has a UIPanGestureRecognizer
that will drag the view horizontally, just updating the x-coordinate.
The problem is that the first UIPanGestureRecognizer
is taking the event to move the view vertically, so I can not use the superview gesture.
I have tried
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:
(UIGestureRecognizer *)otherGestureRecognizer;
and both will work, but I don't want that. I want the horizontally to be detected only if the movement is clearly horizontal. So it would be great if the UIPanGestureRecognizer
had a direction property.
How can I achieve this behavior? I find the docs very confusing, so maybe someone can explain it better here.
Just do this for the vertical pan gesture recognizer, it works for me:
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)panGestureRecognizer {
CGPoint velocity = [panGestureRecognizer velocityInView:someView];
return fabs(velocity.y) > fabs(velocity.x);
}
And for Swift:
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIPanGestureRecognizer) -> Bool {
let velocity = gestureRecognizer.velocity(in: someView)
return abs(velocity.x) > abs(velocity.y)
}
I created a solution with subclassing like in the answer @LocoMike provided, but used the more effective detection mechanism via initial velocity as provided by @Hejazi. I'm also using Swift, but this should be easy to translate to Obj-C if desired.
Advantages over other solutions:
- Simpler and more concise than other subclassing solutions. No additional state to manage.
- Direction detection happens prior to sending Began action, so your pan gesture selector receives no messages if the wrong direction is swiped.
- After initial direction is determined, direction logic is no longer consulted. This results in the generally desired behavior of activating your recognizer if the initial direction is correct, but does not cancel the gesture after it has begun if a user's finger doesn't travel perfectly along the direction.
Here's the code:
import UIKit.UIGestureRecognizerSubclass
enum PanDirection {
case vertical
case horizontal
}
class PanDirectionGestureRecognizer: UIPanGestureRecognizer {
let direction: PanDirection
init(direction: PanDirection, target: AnyObject, action: Selector) {
self.direction = direction
super.init(target: target, action: action)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if state == .began {
let vel = velocity(in: view)
switch direction {
case .horizontal where fabs(vel.y) > fabs(vel.x):
state = .cancelled
case .vertical where fabs(vel.x) > fabs(vel.y):
state = .cancelled
default:
break
}
}
}
}
Example of usage:
let panGestureRecognizer = PanDirectionGestureRecognizer(direction: .horizontal, target: self, action: #selector(handlePanGesture(_:)))
panGestureRecognizer.cancelsTouchesInView = false
self.view.addGestureRecognizer(panGestureRecognizer)
func handlePanGesture(_ pan: UIPanGestureRecognizer) {
let percent = max(pan.translation(in: view).x, 0) / view.frame.width
switch pan.state {
case .began:
...
}
I figured it out creating a subclass of UIPanGestureRecognizer
DirectionPanGestureRecognizer:
#import <Foundation/Foundation.h>
#import <UIKit/UIGestureRecognizerSubclass.h>
typedef enum {
DirectionPangestureRecognizerVertical,
DirectionPanGestureRecognizerHorizontal
} DirectionPangestureRecognizerDirection;
@interface DirectionPanGestureRecognizer : UIPanGestureRecognizer {
BOOL _drag;
int _moveX;
int _moveY;
DirectionPangestureRecognizerDirection _direction;
}
@property (nonatomic, assign) DirectionPangestureRecognizerDirection direction;
@end
DirectionPanGestureRecognizer.m:
#import "DirectionPanGestureRecognizer.h"
int const static kDirectionPanThreshold = 5;
@implementation DirectionPanGestureRecognizer
@synthesize direction = _direction;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesMoved:touches withEvent:event];
if (self.state == UIGestureRecognizerStateFailed) return;
CGPoint nowPoint = [[touches anyObject] locationInView:self.view];
CGPoint prevPoint = [[touches anyObject] previousLocationInView:self.view];
_moveX += prevPoint.x - nowPoint.x;
_moveY += prevPoint.y - nowPoint.y;
if (!_drag) {
if (abs(_moveX) > kDirectionPanThreshold) {
if (_direction == DirectionPangestureRecognizerVertical) {
self.state = UIGestureRecognizerStateFailed;
}else {
_drag = YES;
}
}else if (abs(_moveY) > kDirectionPanThreshold) {
if (_direction == DirectionPanGestureRecognizerHorizontal) {
self.state = UIGestureRecognizerStateFailed;
}else {
_drag = YES;
}
}
}
}
- (void)reset {
[super reset];
_drag = NO;
_moveX = 0;
_moveY = 0;
}
@end
This will only trigger the gesture if the user starts dragging in the selected behavior. Set the direction property to a correct value and you are all set.