How to initiate a gesture recognizer when a gesture is already active? - ios

I have a Subview A(self.thumbnailImageView) added to my superview. I have added a UILongPressGestureRecognizer and UISwipeGestureRecognizer to my subview A.
[self addLongPressGestureRecognizerForPreviewCell:self.thumbnailImageView];
[self addSwipeUpGestureRecognizerForImageView:self.thumbnailImageView];
Now in the handler methods, when the UILongPressGestureRecognizer state begins, i add a Subview B(bigPreviewImage) to my superview(self.view).
-(void)tapGesture:(UILongPressGestureRecognizer *)recognizer{
if (recognizer.state == UIGestureRecognizerStateBegan)
{
// Long press detected, start the timer
[self showPreviewImage:recognizer];
}
else if(recognizer.state == UIGestureRecognizerStateEnded)
{
[self hidePreviewImage];
}
}
-(void)showPreviewImage:(UILongPressGestureRecognizer *)recognizer{
UIImageView *bigPreviewImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Desktop"]];
bigPreviewImage.frame = CGRectMake(self.thumbnailImageView.frame.origin.x - 50.0, self.thumbnailImageView.frame.origin.y + self.thumbnailImageView.frame.size.height + 10.0, 300.0, 250.0);
bigPreviewImage.tag = 10000;//200.0 & 125.0
[bigPreviewImage setUserInteractionEnabled:YES];
[self.view addSubview:bigPreviewImage];
}
Now once the UILongPressGestureRecognizer is active and the user with his finger still pressing the Subview A, If the user swipes up the screen, i want the SwipeGestureRecognizer to get initiated. But the same is not happening. How to initiate a gesture recognizer when a gesture is already active?
I have implemented the shouldRecognizeSimultaneouslyWithGestureRecognizer method but still the swipe up gesture method is not called. Please let me know if i am missing something.

Got it!!!
We dont need a separate Swipe Gesture Recognizer. The different states in Long Press Gesture Recognizer can be used to handle this scenario.
Long Press Gesture has different states Like UIGestureRecognizerStateBegan, UIGestureRecognizerStateChanged and UIGestureRecognizerStateEnded.
UIGestureRecognizerStateBegan gets called as soon as you long press the subview.
UIGestureRecognizerStateChanged gets called when the user tries to move the finger .
UIGestureRecognizerStateEnded gets called when the user lifts the finger from the touch point.
-(void)longPressGestureForPreviewImageView:(UILongPressGestureRecognizer *)recognizer{
if (recognizer.state == UIGestureRecognizerStateBegan)
{
// Long press detected, start the timer
[self showPreviewImage:recognizer];
}
else if(recognizer.state == UIGestureRecognizerStateChanged)
{
NSLog(#"Swipe up");
if ([self.thumbnailImageView.gestureRecognizers containsObject:recognizer]) {
[self.thumbnailImageView removeGestureRecognizer:recognizer];
}
}
else if(recognizer.state == UIGestureRecognizerStateEnded)
{
[self hidePreviewImage];
}
So we can use the Gesture Delegate methods to handle the swipe along with a Long Press Gesture Recognizer.

Related

Enable UIPanGestureRecognizer when did longPress

I'd like to enable UIPanGestureRecognizer on customView when the customView did longPress.
(I wish when you longPress customView, the customView will switch to "move mode", and you can move the customView by drag.)
But in this code, only longPressAction: called. panAction: did not called.
How do I fix it to enable PanAction:?
- (void)viewDidLoad
{
[self.view addSubview:customView];
UILongPressGestureRecognizer *longPressRecognizer = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:#selector(longPressAction:)];
[customView addGestureRecognizer:longPressRecognizer];
UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:#selector(panAction:)];
[customView addGestureRecognizer:panRecognizer];
}
- (void)longPressAction:(UILongPressGestureRecognizer *)recognizer
{
if ([recognizer state] == UIGestureRecognizerStateBegan) {
CustomView *customView = (CustomView *)recognizer.view;
customView.panRecongnizerEnabled = YES; //panRecongnizerEnabled is CustomView's property
}
if ([recognizer state] == UIGestureRecognizerStateEnded) {
CustomView *customView = (CustomView *)recognizer.view;
customView.panRecongnizerEnabled = NO;
}
}
- (void)panAction:(UIPanGestureRecognizer *)recognizer
{
CustomView *customView = (CustomView *)recognizer.view;
if (customCell.panRecongnizerEnabled == NO) return;
NSLog(#"running panAction");
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
Your ViewController needs to conform to the UIGestureRecognizerDelegate. I suspect you either already did that or otherwise the shouldRecognizeSimultaneouslyWithGestureRecognizer would not make any sense. But what you are definitely missing is setting the gestureRecognizer´s delegate to your viewController:
longPressRecognizer.delegate = self;
panRecognizer.delegate = self;
Now you should be receiving both long press and pan simultaneous.
Note: I tested without any customView, just added them to self.view. At least in that case, the code above worked as expected.
I know this question has been closed for a while, but I recently had to do something similar, and there is a much cleaner solution. The UILongPressGestureRecognizer is all you need to do this.
Long-press gestures are continuous. The gesture begins
(UIGestureRecognizerStateBegan) when the number of allowable fingers
(numberOfTouchesRequired) have been pressed for the specified period
(minimumPressDuration) and the touches do not move beyond the
allowable range of movement (allowableMovement). The gesture
recognizer transitions to the Change state whenever a finger moves,
and it ends (UIGestureRecognizerStateEnded) when any of the fingers
are lifted.
Because of this, for what you need, there is no need for the UIPanGestureRecognizer. Simply add the UILongPressGestureRecognizer and attach it to your main view.
- (void)viewDidLoad {
[super viewDidLoad];
UILongPressGestureRecognizer *longPressRecognizer = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:#selector(longPressAction:)];
// set this to your desired delay before the pan action will start.
longPressRecognizer.minimumPressDuration = 0.5;
[self.view addGestureRecognizer:longPressRecognizer];
}
Then, implement the longPressAction method to look at not just UIGestureRecognizerStateBegan or UIGestureRecognizerStateEnded states, but also the UIGestureRecognizerStateChanged state:
- (void)longPressAction:(UIGestureRecognizer *)recognizer
{
if ([recognizer state] == UIGestureRecognizerStateBegan || [recognizer state] == UIGestureRecognizerStateChanged ) {
CGPoint touchPoint = [recognizer locationInView:self.view];
[self.viewToMove setCenter: touchPoint];
NSLog(#"running panAction");
}
}
In my example, I simply move a dummy view to track the center of the user's finger, but you can put any logic you would have put in the UIPanGestureRecognizer's code. Simply put it inside the if block and it greatly simplifies your code and you don't need to deal with interactions between the two gesture recognizers. Your code would also result in many needless calls to the panAction method when the user is simply moving their finger around the screen (but hasn't done a long press).
You can do it by enabling the pan after the long press. Anyhow you will have to save a flag to know if the longPress was called. And in order to enable the pan gesture after the longPress, you can play with the pan gesture delegate, as following:
let previewLongPress = UILongPressGestureRecognizer(target: self, action: #selector(onLongPressed(sender:)))
let previewPanGesture = UIPanGestureRecognizer(target: self, action: #selector(onPanGesture(sender:)))
previewPanGesture?.delegate = self
Your delegate should seems like this:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return gestureRecognizer == previewPanGesture && otherGestureRecognizer == previewLongPress
}
And now you're able to have a pan gesture after a long press gesture.
Hope it helps other people.

Swipe after long press

Ok so I am helping convert an android game to iOS. The game is based on 2048, but with letters instead of numbers. I have a good bit of it working but am still learning Objective C/iOS quirks. So far I have the tiles/grid working, movement is working, etc but I need a bit of help. The goal is to allow the user to long-press on a tile to select it, then slide their finger to an adjacent tile to begin spelling a word. I have the long press portion implemented but I'm at a bit of a loss on how to get it to long-press then swipe. On top of this I already have a swipe that allows the user to move the tiles. In searching on here I've seen suggestions about subclassing so I am figuring I need to subclass the UISwipeGestureRecognizer method. I already put in the simultaneously gesture recognizer, but am unsure where to go from here.
So, there are several questions to this.
What would be the best way to do this? Implement a subclass of each UISwipeGestureRecognizer?
Will my current swipe detection interfere? (right now a swipe by itself moves tiles in direction of swipe)
I would guess I need to do a (if long press) then activate subclassed swipe methods?
Any examples to answer the above questions would be of great help. I'm not asking you to do it for me but at least point me in a general direction. Thanks!
Code below.
// Grid.m
#import "Grid.h"
#import "Tile.h"
- (void)didLoadFromCCB {
// listen for swipes to the left
UISwipeGestureRecognizer * swipeLeft= [[UISwipeGestureRecognizer alloc]initWithTarget:self action:#selector(swipeLeft)];
swipeLeft.direction = UISwipeGestureRecognizerDirectionLeft;
[[[CCDirector sharedDirector] view] addGestureRecognizer:swipeLeft];
// listen for swipes to the right
UISwipeGestureRecognizer * swipeRight= [[UISwipeGestureRecognizer alloc]initWithTarget:self action:#selector(swipeRight)];
swipeRight.direction = UISwipeGestureRecognizerDirectionRight;
[[[CCDirector sharedDirector] view] addGestureRecognizer:swipeRight];
// listen for swipes up
UISwipeGestureRecognizer * swipeUp= [[UISwipeGestureRecognizer alloc]initWithTarget:self action:#selector(swipeUp)];
swipeUp.direction = UISwipeGestureRecognizerDirectionUp;
[[[CCDirector sharedDirector] view] addGestureRecognizer:swipeUp];
// listen for swipes down
UISwipeGestureRecognizer * swipeDown= [[UISwipeGestureRecognizer alloc]initWithTarget:self action:#selector(swipeDown)];
swipeDown.direction = UISwipeGestureRecognizerDirectionDown;
[[[CCDirector sharedDirector] view] addGestureRecognizer:swipeDown];
// listen for long press
UILongPressGestureRecognizer *longpress = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:#selector(onLongPress:)];
[longpress setMinimumPressDuration:0.5];
[[[CCDirector sharedDirector] view] addGestureRecognizer:longpress];
}
- (void)swipeLeft {
[self move:ccp(-1, 0)];
}
- (void)swipeRight {
[self move:ccp(1, 0)];
}
- (void)swipeDown {
[self move:ccp(0, -1)];
}
- (void)swipeUp {
[self move:ccp(0, 1)];
}
// detect longpress, convert to NodeSpace and check if touch location is within tile boundingbox. If yes, set background white, text black.
- (void)onLongPress:(UILongPressGestureRecognizer *) recognizer {
CGPoint touchPoint = [[CCDirector sharedDirector] convertToGL:[recognizer locationInView:[recognizer view]]];
touchPoint = [self convertToNodeSpace:touchPoint];
if (recognizer.state == UIGestureRecognizerStateBegan) {
for (Tile *tile in self.children) {
if([tile isKindOfClass:[Tile class]]) {
CGRect tileBoundingBox = tile.boundingBox;
if (CGRectContainsPoint(tileBoundingBox, touchPoint)) {
tile.backgroundNode.color = [CCColor whiteColor];
tile.valueLabel.color = [CCColor blackColor];
[self spellWord:tile.value];
[_word setString:[_word lowercaseString]];
CCLOG(#"%#", _word);
}
}
}
}
if (recognizer.state == UIGestureRecognizerStateChanged) {
}
if (recognizer.state == UIGestureRecognizerStateEnded) {
for (Tile *tile in self.children) {
if([tile isKindOfClass:[Tile class]]) {
CGRect tileBoundingBox = tile.boundingBox;
if (CGRectContainsPoint(tileBoundingBox, touchPoint)) {
tile.backgroundNode.color = [tile getColor:tile.value];
tile.valueLabel.color = [self getContrastColor:r green:g blue:b];
}
}
}
}
}
// allow for simultaneous gestures
- (BOOL)gestureRecognizer:(UIGestureRecognizer *) recognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
In answer to your questions:
This doesn't strike me as a coding situation that requires one to subclass UILongPressGestureRecognizer. Having said that, subclassing is often a nice way to clean up one's view controller code so you don't have gory gesture recognizer code in the view controller class. But there's nothing here (as I understand it) that demands that. You generally dive into subclassing of gesture recognizers where you need some special custom behavior (e.g. have the gesture fail if some complicated criterion fails). I'd first see if you could achieve the desired UX with standard gestures before I went down that road, though.
The only reason I could see the swipe gestures interfering with each other is that you've specified that shouldRecognizeSimultaneouslyWithGestureRecognizer should return YES. That's used in cases where you need multiple recognizers running at the same, which doesn't seem necessary here (and only a source of problems).
It's unclear to me as to whether you really wanted a separate swipe gesture or whether you just wanted a single gesture ("long press and drag"). If you needed that separate swipe gesture, though, you would generally specify the relative priority of gesture recognizers by specifying requireGestureRecognizerToFail (e.g. have the swipe require long press to fail in order for the swipe to be recognized). But if you really only have one gesture ("long press and drag"), then only one gesture recognizer is needed.
It seems unnecessary. If you want to detect movement after the long press has been recognized, you can put that "move after long press" code in the if statement for UIGestureRecognizedStateChanged in your onLongPress, which occurs after the long press has been recognized, but before the user lifts their finger. The UILongPressGestureRecognizer is a continuous gesture recognizer which will continue to get updates as the user's finger moves after the gesture was initially recognized.
I know you didn't ask for code, but if you wanted a swipe gesture, as well as a long press gesture that was, essentially, the idea of picking it up and dragging it, you could do something like the following. Note, I make the swipe gesture require the long press to fail, so if the user is long pressing, that takes precedence, otherwise it does swipe. But you may not need the swipe gesture at all, so if you don't need it, just remove it altogether:
#import <UIKit/UIGestureRecognizerSubclass.h>
- (void)viewDidLoad {
[super viewDidLoad];
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(handleLongPress:)];
[self.view addGestureRecognizer:longPress];
// if you needed a second gesture, a swipe, completely distinct from the long press and drag
// gesture, you could add it like so:
//
// UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(handleSwipe:)];
// [swipe requireGestureRecognizerToFail:longPress];
// // do additional swipe configuration
// [self.view addGestureRecognizer:swipe];
}
- (void)handleSwipe:(UISwipeGestureRecognizer *)gesture
{
// do your separate swipe stuff here
}
- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture
{
static UIView *tileToMove;
static CGPoint startCenter;
static CGPoint startLocation;
CGPoint location = [gesture locationInView:self.view];
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
{
// find the tile
tileToMove = [self findTileToMove:location];
if (tileToMove) {
// if found, capture state ...
startCenter = tileToMove.center;
startLocation = location;
// ... and animate "pick up tile", so the user gets positive feedback
// that the drag/swipe portion of the gesture is starting.
[UIView animateWithDuration:0.25 animations:^{
tileToMove.transform = CGAffineTransformMakeScale(1.2, 1.2);
}];
} else {
gesture.state = UIGestureRecognizerStateFailed;
}
break;
}
case UIGestureRecognizerStateChanged:
{
// move the tile as the user's finger moves
CGPoint translate = CGPointMake(location.x - startLocation.x, location.y - startLocation.y);
// note, if you want to constrain the translation to be, for example, on the
// x-axis alone, you could do something like:
//
// CGPoint translate = CGPointMake(location.x - startLocation.x, 0);
tileToMove.center = CGPointMake(startCenter.x + translate.x, startCenter.y + translate.y);
break;
}
case UIGestureRecognizerStateEnded:
{
// animate "drop the tile"
[UIView animateWithDuration:0.25 animations:^{
tileToMove.transform = CGAffineTransformIdentity;
// if you want the tile to "snap" to some location having let it go,
// set the `center` or `frame` here.
}];
// clear our variables, just in case
tileToMove = nil;
startCenter = CGPointZero;
startLocation = CGPointZero;
break;
}
default:
break;
}
}
- (UIView *)findTileToMove:(CGPoint)location
{
for (UIView *tile in self.tiles) {
if (CGRectContainsPoint(tile.frame, location)) {
return tile;
}
}
return nil;
}
This might not be quite the exact UI you're looking for, but it illustrates:
How to have two gestures, where one requires the other to fail in order to establish a precedence between the gestures (and clearly only an issue if you want two distinct gestures, which you probably don't);
To not have shouldRecognizeSimultaneouslyWithGestureRecognizer method because I don't want them both to be recognized simultaneously. Note, that's only needed if you really need two gestures, which you may or may not need; and
How to have a long press that not only recognizes initial long press, but subsequent swipe/drag movement, too.

Periodic Event While Pan Gesture Hasn't Ended

So my current project has a pan gesture recognizer, and if I have panned to the top of the screen the screen it is supposed to scroll up to account for that gesture. While the gesture hasn't ended and the current position of the gesture remains at the top of the screen, I want to continually keep scrolling. My problem is the gesture recognizer only gets called when the state changes, therefore my content will only scroll if you move back and forth at the top, not continually while the gesture continues to be at the top. Is there any reasonable way to continually call code while the gesture hasn't ended, but isn't necessarily changing? Here is pseudo-code of what I have:
- (void)handleGestureRecognizer:(UIGestureRecognizer *)gesture {
if ( gesture.state == UIGestureRecognizerStateChanged ) {
CGPoint point = [gesture locationInView:self.view];
if (point.y < 100) {
//I would like this code to be called continually while the
//gesture hasn't ended, not necessarily only when it changes
[self updateScrollPosition];
}
}
I can think of a few ghetto ways to do it by setting state bools based on the current state of the recognizer and creating my own timer to periodically check, but it seems pretty hacky and I don't particularly like it, so I'm wondering if anyone could come up with a cleaner solution.
One way to use a timer and feel better about it would be to conceal it a little by using performSelector:withObject:afterDelay:
- (void)stillGesturing {
[self updateScrollPosition];
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self performSelector:#selector(stillGesturing) withObject:nil afterDelay:0.5];
}
// then in the recognizer target
if (gesture.state == UIGestureRecognizerStateEnded) {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
} else if ( gesture.state == UIGestureRecognizerStateChanged ) {
CGPoint point = [gesture locationInView:self.view];
if (point.y < 100) {
//I would like this code to be called continually while the
//gesture hasn't ended, not necessarily only when it changes
[self stillGesturing];
}
}

pressGestureRecognizer and touchesBegan

I have the following problem.
I am using a UILongPressGestureRecognizer to put a UIView into a "toggle mode". If the UIView is in "toggle mode" the user is able to drag the UIView around the screen. For dragging the UIView around the screen I am using the methods touchesBegan, touchesMoved and touchesEnded.
It works, but: I have to lift my finger in order to drag it, because the touchesBegan method got already called and therefore is not called again and therefore I can't drag the UIView around the screen.
Is there any way to manually call touchesBegan after UILongPressGestureRecognizer got triggered (UILongPressGestureRecognizer changes a BOOL value and the touchesBegan only works if this BOOL is set to YES).
UILongPressGestureRecognizer is a continuous gesture recognizer, so rather than resorting to touchesMoved or UIPanGestureRecognizer, just check for UIGestureRecognizerStateChanged, e.g.:
- (void)viewDidLoad
{
[super viewDidLoad];
UILongPressGestureRecognizer *gesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
[self.view addGestureRecognizer:gesture];
}
- (void)handleGesture:(UILongPressGestureRecognizer *)gesture
{
CGPoint location = [gesture locationInView:gesture.view];
if (gesture.state == UIGestureRecognizerStateBegan)
{
// user held down their finger on the screen
// gesture started, entering the "toggle mode"
}
else if (gesture.state == UIGestureRecognizerStateChanged)
{
// user did not lift finger, but now proceeded to move finger
// do here whatever you wanted to do in the touchesMoved
}
else if (gesture.state == UIGestureRecognizerStateEnded)
{
// user lifted their finger
// all done, leaving the "toggle mode"
}
}
I would suggest you to use UIPanGestureRecognizer as it a recommended gesture for dragging.
You can configure the min. and max. number of touches required for a panning, using the following the properties:
maximumNumberOfTouches
minimumNumberOfTouches
You can handle the states like Began, Changed and Ended, like having animation for the required states.
Using the below method translate the point to the UIView in which you want it.
- (void)setTranslation:(CGPoint)translation inView:(UIView *)view
example:
You have to use a global variable to retain the old frame. Get this in UIGestureRecognizerStateBegan.
When the state is UIGestureRecognizerStateChanged. You can use the
-(void) pannningMyView:(UIPanGestureRecognizer*) panGesture{
if(panGesture.state==UIGestureRecognizerStateBegan){
//retain the original state
}else if(panGesture.state==UIGestureRecognizerStateChanged){
CGPoint translatedPoint=[panGesture translationInView:self.view];
//here you manage to get your new drag points.
}
}
Velocity of the drag. Based on the velocity you can provide a animation to show bouncing of a UIView
- (CGPoint)velocityInView:(UIView *)view

UIPinchGestureRecognizer not called

I have a single view on my iOS application, with a mapView in it.
When adding a tap or long press recognizer, the events are properly called.
But not with the pinch event...
UIPinchGestureRecognizer *handlePinchGesture=[[UIPinchGestureRecognizer alloc]initWithTarget:mapView action:#selector(handleGesture:)];
[mapView addGestureRecognizer:handlePinchGesture];
Any idea what I should add ?
Thanks.
Assuming your mapView is an MKMapView, it has its own pinch gesture recognizer for zooming the map.
If you want to add your own recognizer, you have to allow it to recognize simultaneously with the other (mapview-controlled) recognizer. Set your gesture recognizer's delegate and implement gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: (you can just always return YES).
You should also probably set self as the gesture recognizer's target and not the mapView.
In the handleGesture method did you do something like this:
CGFloat beginPinch; //declare this as your ivars
-(void)handleGesture:(UIPinchGestureRecognizer *)pinchRecognizer
{
if (pinchRecognizer.state == UIGestureRecognizerStateBegan)
{
beginPinch = pinchRecognizer.scale;
}
else if (pinchRecognizer.state == UIGestureRecognizerStateEnded)
{
if (pinchRecognizer.scale < beginPinch)
{
//do your stuff
}
}
}

Resources