UISwipeGestureRecognizer only one direction working - ios

So Im making a page with pageControl (it's a page with multiple views with dots indicating which page you're in), my code looks like the following in viewDidLoad:
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:#selector(swipeAction:)];
UIView *temp = [[UIView alloc]initWithFrame:self.view.frame];
temp.backgroundColor = [UIColor clearColor];
[temp addGestureRecognizer:swipe];
[self.view addSubview:temp];
And in the swipeAction selector I have:
- (void)swipeAction: (UISwipeGestureRecognizer *)sender{
NSLog(#"Swipe action called");
if (sender.direction == UISwipeGestureRecognizerDirectionLeft) {
//Do Something
}
else if (sender.direction == UISwipeGestureRecognizerDirectionRight){
//Do Something Else
}
}
To my surprise, this method only works when you swipe to the right (i.e. the else if block gets called). When you swipe left, the swipeAction doesn't even get called! This is strange, why does this happen and how should I change my code? Any reply is appreciated. Thanks a lot!

There's a couple things you should be aware of here. First, you have to create a gesture for each direction that you want to observe. This isn't a big deal though because you can simple give them the same selector, and it will be just like one gesture for two directions.
UISwipeGestureRecognizer *leftSwipe = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:#selector(swipeAction:)];
leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft;
UISwipeGestureRecognizer *rightSwipe = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:#selector(swipeAction:)];
rightSwipe.direction = UISwipeGestureRecognizerDirectionRight;
UIView *temp = [[UIView alloc]initWithFrame:self.view.frame];
temp.backgroundColor = [UIColor clearColor];
[temp addGestureRecognizer:leftSwipe];
[temp addGestureRecognizer:rightSwipe];
[self.view addSubview:temp];
Second, you never specified the direction of the gesture leaving it to default to right (or 1 on the direction enum)
From the documentation:
The default direction is UISwipeGestureRecognizerDirectionRight. See descriptions of UISwipeGestureRecognizerDirection constants for more information.
typedef enum {
UISwipeGestureRecognizerDirectionRight = 1 << 0,
UISwipeGestureRecognizerDirectionLeft = 1 << 1,
UISwipeGestureRecognizerDirectionUp = 1 << 2,
UISwipeGestureRecognizerDirectionDown = 1 << 3
} UISwipeGestureRecognizerDirection;

Swift 4
let swiper = UISwipeGestureRecognizer(target: self, action: #selector(self.swipeFunction(sender:)))
swiper.direction = UISwipeGestureRecognizer.Direction.right
let swipel = UISwipeGestureRecognizer(target: self, action: #selector(self.swipeFunction(sender:)))
swipel.direction = UISwipeGestureRecognizer.Direction.left
UIView().addGestureRecognizer(swiper)
UIView().addGestureRecognizer(swipel)
#objc func swipeFunction(sender: UISwipeGestureRecognizer) {
print(sender)
}
you should be able to figure it out from there, you add a UISwipeGestureRecognizer for each direction to the UIView

swipe.direction sets the direction(s) you're recognizing, it doesn't tell you which direction was swiped. Add this line when creating your recognizer:
swipe.direction = UISwipeGestureRecognizerDirectionLeft|UISwipeGestureRecognizerDirectionRight;
If you need to detect which direction was swiped, just use two different Recognizers, one for left and one for right.

I was facing the same issue and found a way by extending the UIPanGestureRecognizer in order to implement a UIDirectionalSwipeGestureRecognizer as follows. The direction of the swipe is available as a public property of the recognizer's instance:
import UIKit
public final class UIDirectionalSwipeGestureRecognizer: UIPanGestureRecognizer {
public private(set) var direction: UISwipeGestureRecognizer.Direction?
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
direction = nil
if let touch = touches.first {
startTimestamp = touch.timestamp
startLocation = touch.location(in: nil)
}
}
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if
let currentTimestamp = touches.first?.timestamp,
let startTimestamp = startTimestamp,
currentTimestamp - startTimestamp > 0.3
{
touchesCancelled(touches, with: event)
}
}
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
if
let endLocation = touches.first?.location(in: nil),
let startLocation = startLocation,
startLocation.distance(to: endLocation) > 20
{
direction = UISwipeGestureRecognizer.Direction.fromPoint(startLocation, to: endLocation)
}
}
// MARK: - Private
private var startTimestamp: TimeInterval?
private var startLocation: CGPoint?
}
// MARK: - Extensions
public extension CGPoint {
func distanceSquared(to: CGPoint) -> CGFloat {
(to.x - x) * (to.x - x) + (to.y - y) * (to.y - y)
}
func distance(to: CGPoint) -> CGFloat {
sqrt(distanceSquared(to: to))
}
}
extension UISwipeGestureRecognizer.Direction {
public static func fromPoint(_ startPoint: CGPoint, to endPoint: CGPoint) -> Self {
let offset = CGSize(width: endPoint.x - startPoint.x, height: endPoint.y - startPoint.y)
if abs(offset.width) > abs(offset.height) {
return offset.width > 0 ? .right : .left
} else {
return offset.height > 0 ? .up : .down
}
}
}

Related

Custom UIButton .touchDragEnter and .touchDragExit area/size?

Is it possible to customize the area from the button at which it is considered .touchDragExit (or .touchDragEnter) (out of its selectable area?)?
To be more specific, I am speaking about this situation: I tap the UIButton, the .touchDown gets called, then I start dragging my finger away from the button and at some point (some distance away) it will not select anymore (and of course I can drag back in to select...). I would like the modify that distance...
Is this even possible?
You need to overwrite the UIButton continueTracking and touchesEnded functions.
Adapting #Dean's link, the implementation would be as following (swift 4.2):
class ViewController: UIViewController {
#IBOutlet weak var button: DragButton!
override func viewDidLoad() {
super.viewDidLoad()
}
}
class DragButton: UIButton {
private let _boundsExtension: CGFloat = 0 // Adjust this as needed
override open func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let outerBounds: CGRect = bounds.insetBy(dx: CGFloat(-1 * _boundsExtension), dy: CGFloat(-1 * _boundsExtension))
let currentLocation: CGPoint = touch.location(in: self)
let previousLocation: CGPoint = touch.previousLocation(in: self)
let touchOutside: Bool = !outerBounds.contains(currentLocation)
if touchOutside {
let previousTouchInside: Bool = outerBounds.contains(previousLocation)
if previousTouchInside {
print("touchDragExit")
sendActions(for: .touchDragExit)
} else {
print("touchDragOutside")
sendActions(for: .touchDragOutside)
}
} else {
let previousTouchOutside: Bool = !outerBounds.contains(previousLocation)
if previousTouchOutside {
print("touchDragEnter")
sendActions(for: .touchDragEnter)
} else {
print("touchDragInside")
sendActions(for: .touchDragInside)
}
}
return true
}
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: UITouch = touches.first!
let outerBounds: CGRect = bounds.insetBy(dx: CGFloat(-1 * _boundsExtension), dy: CGFloat(-1 * _boundsExtension))
let currentLocation: CGPoint = touch.location(in: self)
let touchInside: Bool = outerBounds.contains(currentLocation)
if touchInside {
print("touchUpInside action")
return sendActions(for: .touchUpInside)
} else {
print("touchUpOutside action")
return sendActions(for: .touchUpOutside)
}
}
}
Try changing the _boundsExtension value
The drag area is exaclty equal to the area define by bounds.
So if you want to customize the drag are simple customise the bounds of your button.

3D touch/Force touch implementation

How can we implement 3D touch to check if the user taps on UIView or force touch on UIView?
Is there a way to do this with UIGestureRecognize or only with UITouch?
You can do it without a designated gesture recognizer. You do not need to adjust the touchesEnded and touchesBegan method, but simply the touchesMoved to obtain the correct values. getting the force of a uitouch from began/ended will return weird values.
UITouch *touch = [touches anyObject];
CGFloat maximumPossibleForce = touch.maximumPossibleForce;
CGFloat force = touch.force;
CGFloat normalizedForce = force/maximumPossibleForce;
then, set a force threshold and compare the normalizedForce to this threshold (0.75 seems fine for me).
The 3D Touch properties are available on UITouch objects.
You can get these touches by overriding a UIView's touchesBegan: and touchesMoved: methods. Not sure what you see in touchesEnded: yet.
If you're willing to create new gesture recognizers, you have full access to the UITouches as exposed in UIGestureRecognizerSubclass.
I'm not sure how you could use the 3D touch properties in a traditional UIGestureRecognizer. Maybe via the UIGestureRecognizerDelegate protocol's gestureRecognizer:shouldReceiveTouch: method.
With Swift 4.2 and iOS 12, a possible way to solve your problem is to create a custom subclass of UIGestureRecognizer that handles Force Touch and add it to your view next to a UITapGestureRecognizer. The following complete code shows how to implement it:
ViewController.swift
import UIKit
class ViewController: UIViewController {
let redView = UIView()
lazy var tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapHandler))
lazy var forceTouchGestureRecognizer = ForceTouchGestureRecognizer(target: self, action: #selector(forceTouchHandler))
override func viewDidLoad() {
super.viewDidLoad()
redView.backgroundColor = .red
redView.addGestureRecognizer(tapGestureRecognizer)
view.addSubview(redView)
redView.translatesAutoresizingMaskIntoConstraints = false
redView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
redView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
redView.widthAnchor.constraint(equalToConstant: 100).isActive = true
redView.heightAnchor.constraint(equalToConstant: 100).isActive = true
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.forceTouchCapability == UIForceTouchCapability.available {
redView.addGestureRecognizer(forceTouchGestureRecognizer)
} else {
// When force touch is not available, remove force touch gesture recognizer.
// Also implement a fallback if necessary (e.g. a long press gesture recognizer)
redView.removeGestureRecognizer(forceTouchGestureRecognizer)
}
}
#objc func tapHandler(_ sender: UITapGestureRecognizer) {
print("Tap triggered")
}
#objc func forceTouchHandler(_ sender: ForceTouchGestureRecognizer) {
UINotificationFeedbackGenerator().notificationOccurred(.success)
print("Force touch triggered")
}
}
ForceTouchGestureRecognizer.swift
import UIKit.UIGestureRecognizerSubclass
#available(iOS 9.0, *)
final class ForceTouchGestureRecognizer: UIGestureRecognizer {
private let threshold: CGFloat = 0.75
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if let touch = touches.first {
handleTouch(touch)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if let touch = touches.first {
handleTouch(touch)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
state = UIGestureRecognizer.State.failed
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
state = UIGestureRecognizer.State.failed
}
private func handleTouch(_ touch: UITouch) {
guard touch.force != 0 && touch.maximumPossibleForce != 0 else { return }
if touch.force / touch.maximumPossibleForce >= threshold {
state = UIGestureRecognizer.State.recognized
}
}
}
Sources:
GitHub / FlexMonkey - DeepPressGestureRecognizer
GitHub / ashleymills - ForceTouchGestureRecognizer
Apple Developer Documentation - Implementing a Discrete Gesture Recognizer
I created a UIGestureRecognizer that emulates the behavior of the Apple Mail app. Upon 3D touch, it starts with a short single pulse vibrate and then an optional secondary action (hardTarget) and pulse called by hard pressing shortly after the initial press.
Adapted from https://github.com/FlexMonkey/DeepPressGestureRecognizer
Changes:
3D touch vibrate pulses like iOS system behavior
touch must come up for it to end, like Apple mail app
threshold defaults to system default level
hard touch triggers hardAction call like mail app
Note: I added the undocumented system sound k_PeakSoundID, but feel free to turn that off if you are uncomfortable using a constant beyond the documented range. I have been using system sounds with undisclosed constants for years, but you are welcomed to turn off the vibration pulses using the vibrateOnDeepPress property.
import UIKit
import UIKit.UIGestureRecognizerSubclass
import AudioToolbox
class DeepPressGestureRecognizer: UIGestureRecognizer {
var vibrateOnDeepPress = true
var threshold: CGFloat = 0.75
var hardTriggerMinTime: TimeInterval = 0.5
var onDeepPress: (() -> Void)?
private var deepPressed: Bool = false {
didSet {
if (deepPressed && deepPressed != oldValue) {
onDeepPress?()
}
}
}
private var deepPressedAt: TimeInterval = 0
private var k_PeakSoundID: UInt32 = 1519
private var hardAction: Selector?
private var target: AnyObject?
required init(target: AnyObject?, action: Selector, hardAction: Selector? = nil, threshold: CGFloat = 0.75) {
self.target = target
self.hardAction = hardAction
self.threshold = threshold
super.init(target: target, action: action)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
if let touch = touches.first {
handle(touch: touch)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
if let touch = touches.first {
handle(touch: touch)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
state = deepPressed ? UIGestureRecognizerState.ended : UIGestureRecognizerState.failed
deepPressed = false
}
private func handle(touch: UITouch) {
guard let _ = view, touch.force != 0 && touch.maximumPossibleForce != 0 else {
return
}
let forcePercentage = (touch.force / touch.maximumPossibleForce)
let currentTime = Date.timeIntervalSinceReferenceDate
if !deepPressed && forcePercentage >= threshold {
state = UIGestureRecognizerState.began
if vibrateOnDeepPress {
AudioServicesPlaySystemSound(k_PeakSoundID)
}
deepPressedAt = Date.timeIntervalSinceReferenceDate
deepPressed = true
} else if deepPressed && forcePercentage <= 0 {
endGesture()
} else if deepPressed && currentTime - deepPressedAt > hardTriggerMinTime && forcePercentage == 1.0 {
endGesture()
if vibrateOnDeepPress {
AudioServicesPlaySystemSound(k_PeakSoundID)
}
//fire hard press
if let hardAction = self.hardAction, let target = self.target {
_ = target.perform(hardAction, with: self)
}
}
}
func endGesture() {
state = UIGestureRecognizerState.ended
deepPressed = false
}
}
// MARK: DeepPressable protocol extension
protocol DeepPressable {
var gestureRecognizers: [UIGestureRecognizer]? {get set}
func addGestureRecognizer(gestureRecognizer: UIGestureRecognizer)
func removeGestureRecognizer(gestureRecognizer: UIGestureRecognizer)
func setDeepPressAction(target: AnyObject, action: Selector)
func removeDeepPressAction()
}
extension DeepPressable {
func setDeepPressAction(target: AnyObject, action: Selector) {
let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: target, action: action, threshold: 0.75)
self.addGestureRecognizer(gestureRecognizer: deepPressGestureRecognizer)
}
func removeDeepPressAction() {
guard let gestureRecognizers = gestureRecognizers else { return }
for recogniser in gestureRecognizers where recogniser is DeepPressGestureRecognizer {
removeGestureRecognizer(gestureRecognizer: recogniser)
}
}
}
The way I am doing this is to use a combination of a UITapGestureRecognizer (provided by Apple) and a DFContinuousForceTouchGestureRecognizer (provided by me).
The DFContinuousForceTouchGestureRecognizer is nice because it provides continuous updates about the pressure changes so you can do things like augment the view as the user varies their pressure on it, as opposed to a single event. If you just want a single event you can ignore eveything in the DFContinuousForceTouchDelegate except the - (void) forceTouchRecognized callback.
https://github.com/foggzilla/DFContinuousForceTouchGestureRecognizer
You can download this and run the sample app on a device that supports force press to see how it feels.
In your UIViewController implement the following:
- (void)viewDidLoad {
[super viewDidLoad];
_forceTouchRecognizer = [[DFContinuousForceTouchGestureRecognizer alloc] init];
_forceTouchRecognizer.forceTouchDelegate = self;
//here to demonstrate how this works alonside a tap gesture recognizer
_tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapped:)];
[self.imageView addGestureRecognizer:_tapGestureRecognizer];
[self.imageView addGestureRecognizer:_forceTouchRecognizer];
}
implement selector for tap gesture
#pragma UITapGestureRecognizer selector
- (void)tapped:(id)sender {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[[UIAlertView alloc] initWithTitle:#"Tap" message:#"YEAH!!" delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil] show];
});
}
Implement the delegate protocol for force touch:
#pragma DFContinuousForceTouchDelegate
- (void)forceTouchRecognized:(DFContinuousForceTouchGestureRecognizer *)recognizer {
self.imageView.transform = CGAffineTransformIdentity;
[self.imageView setNeedsDisplay];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[[UIAlertView alloc] initWithTitle:#"Force Touch" message:#"YEAH!!" delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil] show];
});
}
- (void)forceTouchRecognizer:(DFContinuousForceTouchGestureRecognizer *)recognizer didStartWithForce:(CGFloat)force maxForce:(CGFloat)maxForce {
CGFloat transformDelta = 1.0f + ((force/maxForce) / 3.0f);
self.imageView.transform = CGAffineTransformMakeScale(transformDelta, transformDelta);
[self.imageView setNeedsDisplay];
}
- (void) forceTouchRecognizer:(DFContinuousForceTouchGestureRecognizer *)recognizer didMoveWithForce:(CGFloat)force maxForce:(CGFloat)maxForce {
CGFloat transformDelta = 1.0f + ((force/maxForce) / 3.0f);
self.imageView.transform = CGAffineTransformMakeScale(transformDelta, transformDelta);
[self.imageView setNeedsDisplay];
}
- (void)forceTouchRecognizer:(DFContinuousForceTouchGestureRecognizer *)recognizer didCancelWithForce:(CGFloat)force maxForce:(CGFloat)maxForce {
self.imageView.transform = CGAffineTransformIdentity;
[self.imageView setNeedsDisplay];
}
- (void)forceTouchRecognizer:(DFContinuousForceTouchGestureRecognizer *)recognizer didEndWithForce:(CGFloat)force maxForce:(CGFloat)maxForce {
self.imageView.transform = CGAffineTransformIdentity;
[self.imageView setNeedsDisplay];
}
- (void)forceTouchDidTimeout:(DFContinuousForceTouchGestureRecognizer *)recognizer {
self.imageView.transform = CGAffineTransformIdentity;
[self.imageView setNeedsDisplay];
}
Note that this will only be useful on a device that supports force touch.
Also you should not add the DFContinuousForceTouchGestureRecognizer to a view if are you running on iOS 8 or under since it uses the new force property on UITouch only available in iOS 9.
If you add this on iOS 8 it will crash, so conditionally add this recognizer based on what iOS version you are running on if you are supporting versions older than iOS 9.

How do I implement velocityInView: for a custom gesture recognizer?

I am implementing a custom UIGestureRecognizer subclass.
I would like to implement velocityInView: the same way that UIPanGestureRecognizer has done it. But I'm not sure how to go about doing it. How do I calculate the velocity in points / second?
Firstly, if you're using Swift, you're going to need to create a Bridging Header and #import <UIKit/UIGestureRecognizerSubclass.h> so you can override UIGestureRegoniser's touches began/moved/cancelled/ended methods. If you're using Objective-C just put that import statement in the .h or .m file.
Secondly, once you've created your UIGestureRecognizer subclass you need these variables in it:
private lazy var displayLink: CADisplayLink = {
let link = CADisplayLink(target: self, selector: Selector("timerCalled"))
link.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
return link
}()
private var previousTouchLocation: CGPoint?
private var currentTouchLocation : CGPoint?
private var previousTime: Double?
private var currentTime : Double?
The display link is for updating the currentTime and previousTime.
Thirdly, to continue setting everything up you need to override the following methods, I think it's all fairly self-explanatory:
override func touchesBegan(touches: Set<NSObject>!, withEvent event: UIEvent!) {
self.state = .Began
displayLink.paused = false
}
override func touchesMoved(touches: Set<NSObject>!, withEvent event: UIEvent!) {
if let view = self.view,
touch = touches.first as? UITouch {
self.state = .Changed
let newLocation = touch.locationInView(view)
self.previousTouchLocation = self.currentTouchLocation
self.currentTouchLocation = newLocation
}
}
override func touchesEnded(touches: Set<NSObject>!, withEvent event: UIEvent!) {
self.state = .Ended
displayLink.paused = true
}
override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent!) {
self.state = .Cancelled
displayLink.paused = true
}
Fourthly, now we can get to the more interesting bit - calculating the velocity:
func velocityInView(targetView: UIView) -> CGPoint {
var velocity = CGPoint.zeroPoint
if let view = self.view,
prevTouchLocation = self.previousTouchLocation,
currTouchLocation = self.currentTouchLocation,
previousTime = self.previousTime,
currentTime = self.currentTime {
let targetPrevLocation = view.convertPoint(prevTouchLocation, toView: targetView)
let targetCurrLocation = view.convertPoint(currTouchLocation, toView: targetView)
let deltaTime = CGFloat(currentTime - previousTime)
let velocityX = (targetCurrLocation.x - targetPrevLocation.x) / deltaTime
let velocityY = (targetCurrLocation.y - targetPrevLocation.y) / deltaTime
velocity = CGPoint(x: velocityX, y: velocityY)
}
return velocity
}
The velocity is calculated using the equation:
velocity = deltaDistance / deltaTime
In this case, take each component of the velocity (x and y) and use that equation to calculate the velocity in each axis. If you wanted to combine both components of the velocity you would use Pythagoras' Theorem like so:
let distance = hypot(targetCurrLocation.x - targetPrevLocation.x,
targetCurrLocation.y - targetPrevLocation.y)
let velocity = distance / deltaTime
Fifthly, in your view controller you can now add your gesture recognizer and use action to get the velocity when dragging over the screen:
override func viewDidLoad() {
super.viewDidLoad()
let recognizer = VelocityGestureRecognizer(target: self, action: Selector("movedGesture:"))
self.view.addGestureRecognizer(recognizer)
}
func movedGesture(recognizer: VelocityGestureRecognizer) {
let v = recognizer.velocityInView(self.view)
println("velocity = \(v)")
}

SpriteKit: add UIGestureRecognizer and detect which node was swiped

In which method do I add a UIGestureRecognizer to my SKScene. And how do I detect which node was swiped? This doesn't seem to work:
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
...
UISwipeGestureRecognizer *recognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(handleSwipe:)];
recognizer.direction = UISwipeGestureRecognizerDirectionUp;
[[self view] addGestureRecognizer:recognizer];
}
return self;
}
You add the UISwipeGestureRecognizer in this method:
- (void)didMoveToView:(SKView *)view
{
UISwipeGestureRecognizer *recognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(handleSwipe:)];
recognizer.direction = UISwipeGestureRecognizerDirectionUp;
[[self view] addGestureRecognizer:recognizer];
}
And that's how you detect which SKNode was swiped:
- (void)handleSwipe:(UISwipeGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
CGPoint touchLocation = [sender locationInView:sender.view];
touchLocation = [self convertPointFromView:touchLocation];
SKSpriteNode *touchedNode = (SKSpriteNode *)[self nodeAtPoint:touchLocation];
NSLog(#"%#", touchedNode);
}
}
Swift 3 version inspired by S.E.'s answer:
func didSceneViewPan(_ sender: UIPanGestureRecognizer) {
if sender.state == .began {
let touchPoint = sender.location(in: sender.view)
let touchLocation = convertPoint(fromView: touchPoint)
if isPointInNode(point: touchLocation, node: testNode) {
print("yes, node touched!")
} else {
print("no bueno :(")
}
}
}
fileprivate func isPointInNode(point: CGPoint, node: SKNode) -> Bool {
// Get all nodes intersecting <point>
let nodes = self.nodes(at: point)
// Return true on first one that matches <node>
for n in nodes {
if n == node {
return true
}
}
// If here, <point> not inside <node> so return false
return false
}
For 2017...
Say you have a scene with various sprites.
Tap on one of them. You need to know which one was tapped ...
class YourScene: SKScene {
override func didMove(to view: SKView) {
super.didMove(to: view)
// your setup for the scene - example, add objects etc
setupTapDetection()
}
func setupTapDetection() {
let t = UITapGestureRecognizer(target: self, action: #selector(tapped(_:)))
view?.addGestureRecognizer(t)
}
#objc func tapped(_ tap: UITapGestureRecognizer) {
// critical...
if tap.state != .ended { return }
// Note: you actually do NOT "invert the Y axis",
// these two calls in fact take care of that properly.
let viewPoint = tap.location(in: tap.view)
let scenePoint = convertPoint(fromView: viewPoint)
// technically there could be more than one node...
let nn = nodes(at: scenePoint)
if nn.count == 0 {
print("you very like tapped 'nowhere'")
return
}
if nn.count != 1 {
// in almost all games or physics scenes,
// it is not possible this would happen
print("something odd happened - overlapping nodes?")
return
}
// you must have a custom class for your sprites
// Say it is class YourSprites: SKSpriteNode
if let dotSpriteTapped = nn.first! as? YourSprites {
// we are done. dotSpriteTapped is the YourSprite,
// which was tapped. now do whatever you want.
let nm = String(describing: dotSpriteTapped.name)
print(" \(nm)")
// you can now access your own properties:
let whichTank = dotSpriteTapped.someID
print(" \(whichTank)")
let tankPower = dotSpriteTapped.sheildStrength
print(" \(tankPower)")
}
}
Enjoy
You can check of the nodes at a location include a node you're looking for using this:
nodes(at: touchLocation).contains(nodeYouAreLookingFor)

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.y) > abs(velocity.x)
}
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.
I tried to constrain the valid area horizontally with UIPanGestureRecognizer.
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
UIPanGestureRecognizer *panGesture = (UIPanGestureRecognizer *)gestureRecognizer;
CGPoint velocity = [panGesture velocityInView:panGesture.view];
double radian = atan(velocity.y/velocity.x);
double degree = radian * 180 / M_PI;
double thresholdAngle = 20.0;
if (fabs(degree) > thresholdAngle) {
return NO;
}
}
return YES;
}
Then, only swiping within thresholdAngle degree horizontally can trigger this pan gesture.
Swift 3.0 answer: just handles does the vertical gesture
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let pan = gestureRecognizer as? UIPanGestureRecognizer {
let velocity = pan.velocity(in: self)
return fabs(velocity.y) > fabs(velocity.x)
}
return true
}
The following solution solved my problem:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
if ([gestureRecognizer.view isEqual:self.view] && [otherGestureRecognizer.view isEqual:self.tableView]) {
return NO;
}
return YES;
}
This is actually just check if pan is going on main view or tableView.
Swift 3 version of Lee's answer for the lazy
import UIKit
import UIKit.UIGestureRecognizerSubclass
enum PanDirection {
case vertical
case horizontal
}
class UIPanDirectionGestureRecognizer: 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: self.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
}
}
}
}
Here is a custom pan gesture in Swift 5
U can constraint its direction and the max angle in the direction, you can also constraint its minimum speed in the direction.
enum PanDirection {
case vertical
case horizontal
}
struct Constaint {
let maxAngle: Double
let minSpeed: CGFloat
static let `default` = Constaint(maxAngle: 50, minSpeed: 50)
}
class PanDirectionGestureRecognizer: UIPanGestureRecognizer {
let direction: PanDirection
let constraint: Constaint
init(direction orientation: PanDirection, target: AnyObject, action: Selector, constraint limits: Constaint = Constaint.default) {
direction = orientation
constraint = limits
super.init(target: target, action: action)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
let tangent = tan(constraint.maxAngle * Double.pi / 180)
if state == .began {
let vel = velocity(in: view)
switch direction {
case .horizontal where abs(vel.y)/abs(vel.x) > CGFloat(tangent) || abs(vel.x) < constraint.minSpeed:
state = .cancelled
case .vertical where abs(vel.x)/abs(vel.y) > CGFloat(tangent) || abs(vel.y) < constraint.minSpeed:
state = .cancelled
default:
break
}
}
}
}
call like this:
let pan = PanDirectionGestureRecognizer(direction: .vertical, target: self, action: #selector(self.push(_:)))
view.addGestureRecognizer(pan)
#objc func push(_ gesture: UIPanGestureRecognizer){
if gesture.state == .began{
// command for once
}
}
or
let pan = PanDirectionGestureRecognizer(direction: .horizontal, target: self, action: #selector(self.push(_:)), constraint: Constaint(maxAngle: 5, minSpeed: 80))
view.addGestureRecognizer(pan)
I took Lee Goodrich's answer and extended it as I needed specifically a single direction pan. Use it like this: let pan = PanDirectionGestureRecognizer(direction: .vertical(.up), target: self, action: #selector(handleCellPan(_:)))
I also added some commenting to make it a little clearer what decisions are actually being made.
import UIKit.UIGestureRecognizerSubclass
enum PanVerticalDirection {
case either
case up
case down
}
enum PanHorizontalDirection {
case either
case left
case right
}
enum PanDirection {
case vertical(PanVerticalDirection)
case horizontal(PanHorizontalDirection)
}
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 {
// expecting horizontal but moving vertical, cancel
case .horizontal(_) where fabs(vel.y) > fabs(vel.x):
state = .cancelled
// expecting vertical but moving horizontal, cancel
case .vertical(_) where fabs(vel.x) > fabs(vel.y):
state = .cancelled
// expecting horizontal and moving horizontal
case .horizontal(let hDirection):
switch hDirection {
// expecting left but moving right, cancel
case .left where vel.x > 0: state = .cancelled
// expecting right but moving left, cancel
case .right where vel.x < 0: state = .cancelled
default: break
}
// expecting vertical and moving vertical
case .vertical(let vDirection):
switch vDirection {
// expecting up but moving down, cancel
case .up where vel.y > 0: state = .cancelled
// expecting down but moving up, cancel
case .down where vel.y < 0: state = .cancelled
default: break
}
}
}
}
}
Swift 4.2
The solution is just for only support pan gesture vertically, same as horizontal.
let pan = UIPanGestureRecognizer(target: self, action: #selector(test1))
pan.cancelsTouchesInView = false
panView.addGestureRecognizer(pan)
Solution 1:
#objc func panAction(pan: UIPanGestureRecognizer) {
let velocity = pan.velocity(in: panView)
guard abs(velocity.y) > abs(velocity.x) else {
return
}
}
Solution 2:
[UISwipeGestureRecognizer.Direction.left, .right].forEach { direction in
let swipe = UISwipeGestureRecognizer(target: self, action: #selector(swipeAction))
swipe.direction = direction
panView.addGestureRecognizer(swipe)
pan.require(toFail: swipe)
}
Then the swipe gesture will swallow the pan gesture. Of course, you don't need to do anything in swipeAction.
You can find the direction dragging on UIView through UIPanGestureRecognizer. Please follow the code.
- (void)viewDidLoad {
[super viewDidLoad];
flipFoward = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(doFlipForward:)];
[flipFoward setMaximumNumberOfTouches:1];
[flipFoward setMinimumNumberOfTouches:1];
[flipFoward setDelegate:self];
[self.view addGestureRecognizer:flipFoward];
flipBack = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(doFlipBack:)];
[flipBack setMaximumNumberOfTouches:1];
[flipBack setMinimumNumberOfTouches:1];
[flipBack setDelegate:self];
[self.view addGestureRecognizer:flipBack];
}
#pragma mark -
#pragma mark RESPONDER
-(void)doFlipForward:(UIGestureRecognizer *)aGestureRecognizer{
NSLog(#"doFlipForward");
if([(UIPanGestureRecognizer*)aGestureRecognizer state] == UIGestureRecognizerStateBegan) {
NSLog(#"UIGestureRecognizerStateBegan");
}
if([(UIPanGestureRecognizer*)aGestureRecognizer state] == UIGestureRecognizerStateChanged) {
NSLog(#"UIGestureRecognizerStateChanged");
}
if([(UIPanGestureRecognizer*)aGestureRecognizer state] == UIGestureRecognizerStateEnded) {
NSLog(#"UIGestureRecognizerStateEnded");
}
}
-(void)doFlipBack:(UIGestureRecognizer *)aGestureRecognizer{
NSLog(#"doFlipBack");
if([(UIPanGestureRecognizer*)aGestureRecognizer state] == UIGestureRecognizerStateBegan) {
NSLog(#"UIGestureRecognizerStateBegan1");
}
if([(UIPanGestureRecognizer*)aGestureRecognizer state] == UIGestureRecognizerStateChanged) {
NSLog(#"UIGestureRecognizerStateChanged1");
}
if([(UIPanGestureRecognizer*)aGestureRecognizer state] == UIGestureRecognizerStateEnded) {
NSLog(#"UIGestureRecognizerStateEnded1");
}
}
#pragma mark -
#pragma mark DELEGATE
-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
CGSize size = [self.view bounds].size;
CGFloat touchX = [gestureRecognizer locationInView:self.view].x;
if((gestureRecognizer == flipFoward)
&& touchX >= (size.width - 88.0f))
{
return YES;
}
if((gestureRecognizer == flipBack)
&& touchX <= 88.0f)
{
return YES;
}
return NO;
}
Here is how I resolved:
First I enabled Simultaneously PanGesture Recognition.
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
Then I Isolate Horizontal and Vertical Pan gestures (accumulator is NSMutableArray property):
- (void)verticalPan :(UIPanGestureRecognizer *) sender {
CGPoint touch = [sender translationInView:self];
NSValue *value = [NSValue valueWithCGPoint:touch];
[accumulator addObject:value];
int firstXObjectValue = (int)[[accumulator objectAtIndex:0] CGPointValue].x ;
int lastXObjectValue = (int)[[accumulator lastObject] CGPointValue].x;
int firstYObjectValue = (int)[[accumulator objectAtIndex:0] CGPointValue].y;
int lastYObjectValue = (int)[[accumulator lastObject] CGPointValue].y;
if (abs(lastYObjectValue - firstYObjectValue) < 4 && abs(lastXObjectValue - firstXObjectValue) > 4) {
NSLog(#"Horizontal Pan");
//do something here
}
else if (abs(lastYObjectValue - firstYObjectValue) > 4 && abs(lastXObjectValue - firstXObjectValue) < 4){
NSLog(#"Vertical Pan");
//do something here
}
if (accumulator.count > 3)
[accumulator removeAllObjects];
I pushed an example here:
add custom pan in scrollview
let pangesture = UIPanGestureRecognizer(target: self, action: "dragview:")
yourview.addGestureRecognizer(pangesture)
func dragview(panGestureRecognizer:UIPanGestureRecognizer)
{
let touchlocation = panGestureRecognizer.locationInView(parentview)
yourview.center.y = touchlocation.y //x for horizontal
}
You may use simple panGestureRecognizer. No need to use
pandirectionregognizer or stuff. Just use y value of translationInview
Below code move drag view only up and down
- (void)gesturePan_Handle:(UIPanGestureRecognizer *)gesture {
if (gesture.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [gesture translationInView:gesture.view];
recognizer.view.center = CGPointMake(recognizer.view.center.x, recognizer.view.center.y + translation.y);
[gesture setTranslation:CGPointMake(0, 0) inView:gesture.view];
}
}
- (void)dragAction:(UIPanGestureRecognizer *)gesture{
UILabel *label = (UILabel *)gesture.view;
CGPoint translation = [gesture translationInView:label];
label.center = CGPointMake(label.center.x + translation.x,
label.center.y + 0);
[gesture setTranslation:CGPointZero inView:label];}
I created PanGestureRecognizer #selector action method for the object that needed only Horizontal scrolling.
UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc]initWithTarget:self action:#selector(smileyDragged:)];
[buttonObject addGestureRecognizer:gesture];
Swift way
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer {
return isVerticalGesture(panGestureRecognizer)
}
return false
}
private func isVerticalGesture(_ recognizer: UIPanGestureRecognizer) -> Bool {
let translation = recognizer.translation(in: superview!)
if fabs(translation.y) > fabs(translation.x) {
return true
}
return false
}
For all you Swift users out there, this will do the job :)
import Foundation
import UIKit.UIGestureRecognizerSubclass
class DirectionPanGestureRecognizer: UIPanGestureRecognizer {
let kDirectionPanThreshold = CGFloat(5)
var drag = true
var moveX = CGFloat(0)
var moveY = CGFloat(0)
override init(target: AnyObject, action: Selector) {
super.init(target: target, action: action)
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
super.touchesMoved(touches, withEvent: event)
if state == .Failed {
return
}
let nowPoint = touches.anyObject()?.locationInView(view)
let prevPoint = touches.anyObject()?.previousLocationInView(view)
moveX += prevPoint!.x - nowPoint!.x
moveY += prevPoint!.y - nowPoint!.y
if !drag {
if abs(moveX) > kDirectionPanThreshold {
state = .Failed
} else {
drag = true
}
}
}
override func reset() {
super.reset()
moveX = 0
moveY = 0
drag = false
}
}
I took an excellent answer by Lee Goodrich and ported to Swift 3
import UIKit
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: self.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
}
}
}
}
I would love to share my approach because all other approaches are based on either UIGestureRecognizerDelegate or subclassing UIPanGestureRecognizer.
My approach is based on runtime and swizzling. I'm not 100% sure about this approach, but you can test and improve it yourself.
Set the direction of any UIPanGestureRecognizer with just one line of code:
UITableView().panGestureRecognizer.direction = UIPanGestureRecognizer.Direction.vertical
use pod 'UIPanGestureRecognizerDirection' or the code:
public extension UIPanGestureRecognizer {
override open class func initialize() {
super.initialize()
guard self === UIPanGestureRecognizer.self else { return }
func replace(_ method: Selector, with anotherMethod: Selector, for clаss: AnyClass) {
let original = class_getInstanceMethod(clаss, method)
let swizzled = class_getInstanceMethod(clаss, anotherMethod)
switch class_addMethod(clаss, method, method_getImplementation(swizzled), method_getTypeEncoding(swizzled)) {
case true:
class_replaceMethod(clаss, anotherMethod, method_getImplementation(original), method_getTypeEncoding(original))
case false:
method_exchangeImplementations(original, swizzled)
}
}
let selector1 = #selector(UIPanGestureRecognizer.touchesBegan(_:with:))
let selector2 = #selector(UIPanGestureRecognizer.swizzling_touchesBegan(_:with:))
replace(selector1, with: selector2, for: self)
let selector3 = #selector(UIPanGestureRecognizer.touchesMoved(_:with:))
let selector4 = #selector(UIPanGestureRecognizer.swizzling_touchesMoved(_:with:))
replace(selector3, with: selector4, for: self)
}
#objc private func swizzling_touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
self.swizzling_touchesBegan(touches, with: event)
guard direction != nil else { return }
touchesBegan = true
}
#objc private func swizzling_touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
self.swizzling_touchesMoved(touches, with: event)
guard let direction = direction, touchesBegan == true else { return }
defer {
touchesBegan = false
}
let forbiddenDirectionsCount = touches
.flatMap({ ($0.location(in: $0.view) - $0.previousLocation(in: $0.view)).direction })
.filter({ $0 != direction })
.count
if forbiddenDirectionsCount > 0 {
state = .failed
}
}
}
public extension UIPanGestureRecognizer {
public enum Direction: Int {
case horizontal = 0
case vertical
}
private struct UIPanGestureRecognizerRuntimeKeys {
static var directions = "\(#file)+\(#line)"
static var touchesBegan = "\(#file)+\(#line)"
}
public var direction: UIPanGestureRecognizer.Direction? {
get {
let object = objc_getAssociatedObject(self, &UIPanGestureRecognizerRuntimeKeys.directions)
return object as? UIPanGestureRecognizer.Direction
}
set {
let policy = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
objc_setAssociatedObject(self, &UIPanGestureRecognizerRuntimeKeys.directions, newValue, policy)
}
}
fileprivate var touchesBegan: Bool {
get {
let object = objc_getAssociatedObject(self, &UIPanGestureRecognizerRuntimeKeys.touchesBegan)
return (object as? Bool) ?? false
}
set {
let policy = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
objc_setAssociatedObject(self, &UIPanGestureRecognizerRuntimeKeys.touchesBegan, newValue, policy)
}
}
}
fileprivate extension CGPoint {
var direction: UIPanGestureRecognizer.Direction? {
guard self != .zero else { return nil }
switch fabs(x) > fabs(y) {
case true: return .horizontal
case false: return .vertical
}
}
static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}
}
I tried this: which worked for me as per the question describes
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIPanGestureRecognizer {
return true
} else {
return false
}
}
SWIFT 4.2
I went further and make a direction Pan Gesture:
enum PanDirection {
case up
case left
case right
case down
}
class PanDirectionGestureRecognizer: UIPanGestureRecognizer {
fileprivate 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)
guard state != .failed else { return }
let vel = velocity(in: view)
let velocities: [PanDirection: CGFloat]
= [.up: -vel.y,
.left: -vel.x,
.right: vel.x,
.down: vel.y]
let sortedKeys = velocities.sorted { $0.1 < $1.1 }
if let key = sortedKeys.last?.key,
key != direction {
state = .cancelled
}
}
}
(Used: https://github.com/fastred/SloppySwiper and https://stackoverflow.com/a/30607392/5790492)
PanGestureRecognizer interface contains the following definitions:
unsigned int _canPanHorizontally:1;
unsigned int _canPanVertically:1;
I didn't check this, but maybe it's accessible via subclass.

Resources