Is there something that will repeat touchesBegan? - ios

I have a button that when getting pressed will move a node. How can I repeat this non stop while button is pressed. I guess I'm looking for something between touchesBegan and touchesEnded. Something like touchesContinue because I'd like the node to continue moving while the button is pressed. This is what I have so far.
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
// 1
leftMove.name = "Left"
rightMove.name = "Right"
/* Called when a touch begins */
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
if (node.name == "Left") {
// Implement your logic for left button touch here:
player.position == CGPoint(x:player.position.x-1, y:player.position.y)
} else if (node.name == "Right") {
// Implement your logic for right button touch here:
player.position = CGPoint(x:player.position.x+1, y:player.position.y)
}
}

Use NSTimer to repeat the move action. Have the timer started on the Touch Down and have the timer stopped on either Touch Up Inside or Touch Up Outside.
import UIKit
class ViewController: UIViewController, UIScrollViewDelegate {
var button: UIButton!
var timer: NSTimer!
leftMove.name = "Left"
rightMove.name = "Right"
override func viewDidLoad() {
super.viewDidLoad()
button = UIButton(frame: CGRectMake(100, 100, 50, 50)) as UIButton
//add Target for .TouchDown events, when user touch down
button.addTarget(self, action: #selector(ViewController.touchDown(_:)), forControlEvents: .TouchDown)
//add Target for .TouchUpInside events, when user release the touch
button.addTarget(self, action: #selector(ViewController.release(_:)), forControlEvents: .TouchUpInside)
view.addSubview(button)
}
func touchDown(sender: UIButton) {
//start the timer
timer = NSTimer.scheduledTimerWithTimeInterval(0.3, target: self, selector: #selector(ViewController.movePlayer(_:)), userInfo: nil, repeats: true)
}
func release(sender: UIButton) {
//stop the timer
self.timer.invalidate()
self.timer = nil
}
func movePlayer(sender: NSTimer) {
//move your stuff here
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
if (node.name == "Left") {
// Implement your logic for left button touch here:
player.position == CGPoint(x:player.position.x-1, y:player.position.y)
} else if (node.name == "Right") {
// Implement your logic for right button touch here:
player.position = CGPoint(x:player.position.x+1, y:player.position.y)
}
}
}

THIS IS IN OBJECTIVE C
i suggest you to use this i did it with touch down event
create Timer object global
TOUCH UP INSIDE and Drag and EXIT
- (IBAction)arrowUpUpperLipTapped:(UIButton *)sender {
NSLog(#"SIngle TAPPED");
if (self.timer1) {
[self.timer1 invalidate];
self.timer1 = nil;
}
[self arrowUpperLipPosChanged:sender];
}
//-----------------------------------------------------------------------
TOUCH DOWN
- (IBAction)arrowUpperLipLongTapped:(UIButton *)sender {
NSLog(#"LONG TAPPED");
if (self.timer) {
[self.timer1 invalidate];
self.timer1 = nil;
}
_timer1 = [NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:#selector(arrowUpperLipTimerAction:) userInfo:sender repeats:YES];
}
//-----------------------------------------------------------------------
- (void) arrowUpperLipPosChanged :(UIButton *) sender {
CGRect frame = self.viewUpperLip.frame;
if (sender.tag == 9001) {
// UP Arrow Tapped
frame.origin.y --;
} else if (sender.tag == 9002) {
frame.origin.y ++;
}
if (!CGRectContainsPoint(CGRectMake(frame.origin.x, frame.origin.y + frame.size.height / 2, frame.size.width, frame.size.height), self.imgViewGridHEyesCenter.center) && !(CGRectIntersection(frame, [self.view viewWithTag:203].frame).size.height >= frame.size.height)) {
[self.viewUpperLip setFrame:frame];
}
}
//-----------------------------------------------------------------------
- (void) arrowUpperLipTimerAction :(NSTimer *) timer {
UIButton *sender = (UIButton *)[timer userInfo];
[self arrowUpperLipPosChanged:sender];
}

Related

UIBarButtonItem Long Press / Short Press

I have looked through the existing questions on this subject matter, and there appears to be no answers post iOS 11 (which appeared to break the gesturerecognizers).
Is there a way to detect the short press / long press on a UIBarButtonItem? Apple uses this functionality in Pages, Numbers, Keynote for Undo / Redo.
Just replace your default event handler for the tap event
-(void)handlerForBarButton:(UIBarButtonItem*)p_Sender
with its extend version
-(void)handlerForBarButton:(UIBarButtonItem*)p_Sender forEvent:(UIEvent*)p_Event
By inspecting the UIEvent object you can learn, if the event was a long press (tapCount of first UITouch object is 0).
This is my code for a playback rate button that returns the rate to '1x' if the bar button item is pressed longer the about one second:
/*
rateButtonPressed:forEvent:
*/
- (IBAction)rateButtonPressed:(UIBarButtonItem*)p_Sender
forEvent:(UIEvent*)p_Event
{
UITouch* firstTouch = nil;
if ( (nil != ((firstTouch = p_Event.allTouches.allObjects.firstObject)))
&& (0 == firstTouch.tapCount))
{
self.avAudioPlayer.rate = 1.0;
}
else
{ // Default tap
if (2.0 == self.avAudioPlayer.rate)
{
self.avAudioPlayer.rate = 0.5;
}
else
{
self.avAudioPlayer.rate += 0.25;
}
}
[WBSettingsSharedInstance.standardUserDefaults setDouble:self.avAudioPlayer.rate
forKey:#"audioPlayerRate"];
[self updateRateBarButton];
}
Try this
#IBOutlet weak var btn: UIButton!
override func viewDidLoad() {
let tapGesture = UITapGestureRecognizer(target: self, #selector (tap)) //Tap function will call when user tap on button
let longGesture = UILongPressGestureRecognizer(target: self, #selector(long)) //Long function will call when user long press on button.
tapGesture.numberOfTapsRequired = 1
btn.addGestureRecognizer(tapGesture)
btn.addGestureRecognizer(longGesture)
}
#objc func tap() {
print("Single tap done")
}
#objc func long() {
print("Long gesture recognized")
}

Swift: Long Press Gesture Recognizer - Detect taps and Long Press

I want to wire an action such that if the gesture is a tap, it does animates an object in a particular way but if the press duration was more than .5 secs it does something else.
Right now, I just have the animation hooked up. I don't know how I can differentiate between a long press and a tap?
How do I access the press duration to achieve the above?
#IBAction func tapOrHold(sender: AnyObject) {
UIView.animateKeyframesWithDuration(duration, delay: delay, options: options, animations: {
UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0, animations: {
self.polyRotate.transform = CGAffineTransformMakeRotation(1/3 * CGFloat(M_PI * 2))
})
UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0, animations: {
self.polyRotate.transform = CGAffineTransformMakeRotation(2/3 * CGFloat(M_PI * 2))
})
UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0, animations: {
self.polyRotate.transform = CGAffineTransformMakeRotation(3/3 * CGFloat(M_PI * 2))
})
}, completion: { (Bool) in
let vc : AnyObject! = self.storyboard?.instantiateViewControllerWithIdentifier("NextView")
self.showViewController(vc as UIViewController, sender: vc)
})
Define two IBActions and set one Gesture Recognizer to each of them. This way you can perform two different actions for each gesture.
You can set each Gesture Recognizer to different IBActions in the interface builder.
#IBAction func tapped(sender: UITapGestureRecognizer)
{
println("tapped")
//Your animation code.
}
#IBAction func longPressed(sender: UILongPressGestureRecognizer)
{
println("longpressed")
//Different code
}
Through code without interface builder
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "tapped:")
self.view.addGestureRecognizer(tapGestureRecognizer)
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: "longPressed:")
self.view.addGestureRecognizer(longPressRecognizer)
func tapped(sender: UITapGestureRecognizer)
{
println("tapped")
}
func longPressed(sender: UILongPressGestureRecognizer)
{
println("longpressed")
}
Swift 5
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapped))
self.view.addGestureRecognizer(tapGestureRecognizer)
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressed))
self.view.addGestureRecognizer(longPressRecognizer)
#objc func tapped(sender: UITapGestureRecognizer){
print("tapped")
}
#objc func longPressed(sender: UILongPressGestureRecognizer) {
print("longpressed")
}
For swift2
let lpgr = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
lpgr.minimumPressDuration = 0.5
lpgr.delaysTouchesBegan = true
lpgr.delegate = self
self.featuredCouponColView.addGestureRecognizer(lpgr)
Action
//MARK: - UILongPressGestureRecognizer Action -
func handleLongPress(gestureReconizer: UILongPressGestureRecognizer) {
if gestureReconizer.state != UIGestureRecognizerState.Ended {
//When lognpress is start or running
}
else {
//When lognpress is finish
}
}
For Swift 4.2/ Swift 5
let lpgr = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
lpgr.minimumPressDuration = 0.5
lpgr.delaysTouchesBegan = true
lpgr.delegate = self
self.colVw.addGestureRecognizer(lpgr)
//MARK: - UILongPressGestureRecognizer Action -
#objc func handleLongPress(gestureReconizer: UILongPressGestureRecognizer) {
if gestureReconizer.state != UIGestureRecognizer.State.ended {
//When lognpress is start or running
}
else {
//When lognpress is finish
}
}
Through code without interface builder
// Global variables declaration
var longPressed = false
var selectedRow = 0
override func viewDidLoad() {
super.viewDidLoad()
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(ContactListTableViewController.handleLongPress(_:)))
longPressGesture.minimumPressDuration = 1.0 // 1 second press
longPressGesture.allowableMovement = 15 // 15 points
longPressGesture.delegate = self
self.tableView.addGestureRecognizer(longPressGesture)
}
// Long tap work goes here !!
if (longPressed == true) {
if(tableView.cellForRowAtIndexPath(indexPath)?.accessoryType == .Checkmark){
tableView.cellForRowAtIndexPath(indexPath)?.accessoryType = .None
self.selectedRow -= 1
if(self.selectedRow == 0){
self.longPressed = false
}
} else {
self.selectedRow += 1
tableView.cellForRowAtIndexPath(indexPath)?.accessoryType = .Checkmark
}
} else if(self.selectedRow == 0) {
// Single tape work goes here !!
}
But the only problem is the long press gesture runs two times. If you have found any solution do comment below !
Swift 5 using interface builder
for the normal tap you can simply create a touch up inside action from your button.
for the long press, create an outlet for your button, create the tap gesture recognizer and set it to the button then create the selector method to perform the long press tasks.
#IBOutlet var myButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(addToListButtonLongPress(_:)))
longPressRecognizer.numberOfTouchesRequired = 1
longPressRecognizer.allowableMovement = 10
longPressRecognizer.minimumPressDuration = 0.5
myButton.addGestureRecognizer(longPressRecognizer)
}
// Connected to myButton in interface builder.
#IBAction func myButtonTapped(_ sender: UIButton) {
print("button tapped")
}
#objc func myButtonLongPressed(_ sender: UILongPressGestureRecognizer) {
print("button long pressed")
}

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.

Double touch on UIButton

How to recognize double touch on UIButton ?
Add an target-action for the control event UIControlEventTouchDownRepeat, and do action only when the touch's tapCount is 2.
Objective-C:
[button addTarget:self action:#selector(multipleTap:withEvent:)
forControlEvents:UIControlEventTouchDownRepeat];
...
-(IBAction)multipleTap:(id)sender withEvent:(UIEvent*)event {
UITouch* touch = [[event allTouches] anyObject];
if (touch.tapCount == 2) {
// do action.
}
}
As #Gavin commented, double-tap on a button is an unusual gesture. On the iPhone OS double-tap is mostly used for zoomable views to zoom into/out of a region of focus. It may be unintuitive for the users if you make the gesture to perform other actions.
Swift 3:
button.addTarget(self, action: #selector(multipleTap(_:event:)), for: UIControlEvents.touchDownRepeat)
And then:
func multipleTap(_ sender: UIButton, event: UIEvent) {
let touch: UITouch = event.allTouches!.first!
if (touch.tapCount == 2) {
// do action.
}
}
If you are working with swift 5, #kennytm's solution won't work. So you can write an objective-c/swift function and add it as gesture with number of desired taps.
let tap = UITapGestureRecognizer(target: self, action: #selector(doubleTapped))
tap.numberOfTapsRequired = 2
btn.addGestureRecognizer(tap)
and then
#objc func doubleTapped() {
// your desired behaviour.
}
Here button can be tapped any number of times and it will show as tapped. But the above function won't execute until button tapped for required number of times.
[button addTarget:self action:#selector(button_TouchDown:) forControlEvents:UIControlEventTouchDown];
[button addTarget:self action:#selector(button_TouchDownRepeat:withEvent:) forControlEvents:UIControlEventTouchDownRepeat];
-(void)button_one_tap:(UIButton*) button
{
NSLog(#"button_one_tap");
}
-(void)button_double_tap:(UIButton*) button
{
NSLog(#"button_double_tap");
}
-(void)button_TouchDown:(UIButton*) button
{
NSLog(#"button_TouchDown");
[self performSelector:#selector(button_one_tap:) withObject:button afterDelay:0.3 /*NSEvent.doubleClickInterval maybe too long*/];
}
-(void)button_TouchDownRepeat:(UIButton*) button withEvent:(UIEvent*)event {
NSLog(#"button_TouchDownRepeat");
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(button_one_tap:) object:button];
UITouch* touch = [[event allTouches] anyObject];
//NSLog(#"touch.tapCount = %ld", touch.tapCount);
if (touch.tapCount == 2) {
// do action.
[self button_double_tap:button];
}
}
#IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
button.addTarget(self, action: "didTap:", forControlEvents: .TouchUpInside)
button.addTarget(self, action: "didDoubleTap:", forControlEvents: .TouchDownRepeat)
}
var ignoreTap = false
func didTap(sender: UIButton) {
if ignoreTap {
ignoreTap = false
print("ignoretap", sender)
return
}
print("didTap", sender)
}
func didDoubleTap(sender: UIButton) {
ignoreTap = true
print("didDoubleTap", sender)
}
try to use this for the button event
UIControlEventTouchDownRepeat

Resources