Moving UIImageViews in ScrollView - objective-c

In my scrollview im having multiple imageviews where i can able to move each imageviews when user taps LONGPRESS i have done this using,
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(moveImage:)];
[panGesture setMaximumNumberOfTouches:2];
[panGesture setDelegate:self];
[imageview addGestureRecognizer:panGesture];
UILongPressGestureRecognizer *longPressRecognizer = [[UILongPressGestureRecognizer alloc] init];
[imageview addGestureRecognizer:longPressRecognizer];
- (BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
- (void)adjustAnchorPointForGestureRecognizer:(UIGestureRecognizer *)gestureRecognizerv {
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
UIView *piece = gestureRecognizerv.view;
CGPoint locationInView = [gestureRecognizerv locationInView:piece];
CGPoint locationInSuperview = [gestureRecognizerv locationInView:piece.superview];
piece.layer.anchorPoint = CGPointMake(locationInView.x / piece.bounds.size.width, locationInView.y / piece.bounds.size.height);
piece.center = locationInSuperview;
}
}
- (void)moveImage:(UIPanGestureRecognizer *)gestureRecognizerg
{
NSLog(#"sdfgdsgsdg");
UIView *piece = [gestureRecognizerg view];
[self adjustAnchorPointForGestureRecognizer:gestureRecognizerg];
// need to test if the scrollview is already using the touches. If it is, leave them
if (!galleryView.tracking) {
if ([gestureRecognizerg state] == UIGestureRecognizerStateBegan || [gestureRecognizerg state] == UIGestureRecognizerStateChanged) {
CGPoint translation = [gestureRecognizerg translationInView:[piece superview]];
[piece setCenter:CGPointMake([piece center].x + translation.x, [piece center].y + translation.y)];
[gestureRecognizerg setTranslation:CGPointZero inView:[piece superview]];
}
}
}
Here my problem is the THE IMAGEVIEW is moving every where in scollview i need to rearrange the views i.e like Gallery view.

To get that rearrangement behavior you're going to need to have some logic that sets up a grid and 'snaps' elements to it, moves elements out the way when required, etc. It is reasonably complicated. Your current code simply moves views with your gestures.
However, if you are able to target iOS 6 and above you will almost certainly want to use a UICollectionView instead, which gives you a lot of this for free. Collection views simplify the layout and logic behind re-arranging, and will generally make your life a lot easier.

Related

Why does this code not properly center scene when doing UIPanGestureRecognizer?

I am a beginner developing a program for the AppStore using Xcode's sprite kit. I made a 'test' program so I can try out new things before adding it to my game. Right now I am fiddling around with swiping a scene - I have an SKNode (called "background") where I am adding several children as SKSpriteNodes. One sprite node is visible on the initial scene (the center, position 160,240), and two more that are not visible: to the left of the scene (position -160,240), and to the right of the scene (position 480,240).
I would like my game to be able to swipe left or right, and when it swipes left or right, the view will auto-center itself (with animation) to one of the three SKSpriteNodes. My code using the UIPanGestureRecognizer to move the background node works properly, and my code for auto-centering the view works MOSTLY (background position set to 0,0 or -320,0 or +320,0), but sometimes it has a strange offset and doesn't completely center itself (for example, the background position will be 7,0 or -34,0 when I pan right or left). What am I doing wrong?
P.S: I am using code from RayWenderlich's "iOS Games" for the SKTMoveEffect. I also want to note that if I make the function f(t)=t, there is no problem (at least in my several tests), but f(t)=t^2 or anything else seems to have an issue; if it helps to see the code for this I can post it too
#implementation LTMyScene
{
SKNode *background;
SKSpriteNode *spaceship1, *spaceship2;
}
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
/* Setup your scene here */
background=[SKNode node];
[self addChild:background];
self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0];
SKLabelNode *myLabel = [SKLabelNode labelNodeWithFontNamed:#"Chalkduster"];
myLabel.text = #"Hello, World!";
myLabel.fontSize = 30;
myLabel.position = CGPointMake(CGRectGetMidX(self.frame),
CGRectGetMidY(self.frame));
[background addChild:myLabel];
spaceship1=[SKSpriteNode spriteNodeWithImageNamed:#"Spaceship.png"];
spaceship1.position=CGPointMake(-self.size.width/2, self.size.height/2);
spaceship1.anchorPoint=CGPointMake(0.5, 0.5);
[background addChild:spaceship1];
spaceship2=[SKSpriteNode spriteNodeWithImageNamed:#"Spaceship.png"];
spaceship2.position=CGPointMake(self.size.width*3/2, self.size.height/2);
spaceship2.anchorPoint=CGPointMake(0.5, 0.5);
[background addChild:spaceship2];
}
return self;
}
- (void)didMoveToView:(SKView *)view
{
UIPanGestureRecognizer *swipe = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(dragPlayer:)];
[[self view] addGestureRecognizer:swipe];
}
-(void)dragPlayer: (UIPanGestureRecognizer *)gesture {
[[[self view] layer] removeAllAnimations];
CGPoint trans = [gesture translationInView:self.view];
SKAction *moveAction = [SKAction moveByX:trans.x y:0 duration:0];
[background runAction:moveAction];
[gesture setTranslation:CGPointMake(0, 0) inView:self.view];
if([gesture state] == UIGestureRecognizerStateEnded) {
CGFloat finalX=0;
if (abs(background.position.x)<self.size.width/2) {
finalX=0;
} else if (abs(background.position.x)<self.size.width*3/2) {
finalX=self.size.width*background.position.x/abs(background.position.x);
}
NSLog(#"%f",finalX);
SKTMoveEffect *upEffect =
[SKTMoveEffect effectWithNode:background duration:0.5
startPosition:background.position
endPosition:CGPointMake(finalX, 0)];
upEffect.timingFunction = ^(float t) {
// return powf(2.0f, -3.0f * t) * fabsf(cosf(t * M_PI * 1.0f)) //has bounce
// return (-1.0f*t*t+1) //no bounce ... for parabola this is only solution with (1,0) and (0,1) as intercepts and vertex at (1,0)
return (t*t)
;};
SKAction *upAction = [SKAction actionWithEffect:upEffect];
[background runAction:upAction];
}
}
-(void)update:(CFTimeInterval)currentTime {
/* Called before each frame is rendered */
NSLog(#"%f,%f",background.position.x,background.position.y);
}
#end
You have to remember that you are moving the larger background node with other nodes as its children. Keeping that in mind, the code you need to center on a specific node is:
_worldNode.position = CGPointMake(-(myNode.position.x-(self.size.width/2)), -(myNode.position.y-(self.size.height/2)));
The above assumes that your main background "canvas" is called _worldNode and your target node is a child of _worldNode

The UICollectionView "swipe-away" in iOS7 app manager?

On any 2014+ iPhone or iPad, double-click the home button to see the "app manager"
This is a left-right UICollectionView BUT it has a "swipe-away" gesture .. swipe up. How is it done? It's not so easy to "remove" a cell from a UICollectionView.
Footnote for googlers .. for the general problem of "peeling off", "tearing away", one cell from a collection view, here's a full tidy explanation: https://stackoverflow.com/a/24339705/294884 Hope it helps someone.
It can be much simpler than the comments on your question are suggesting.
Your cell should contain a view (the thing that you're going to drag off) and you add a UIPanGestureRecognizer to that view.
In the gesture's action method, you move the view up or down, and when it gets far enough off that you want to delete it, you just animate it off. There are plenty of questions here dealing with this part.
This leaves a gap in your collection and now you need to move things around. It turns out this is quite simple:
[_collectionView performBatchUpdates:^{
[_collectionView deleteItemsAtIndexPaths:#[indexPath]];
} completion:^(BOOL finished) {
// you might want to remove the data from the data source here so the view doesn't come back to life when the collection view is reloaded.
}];
The stuff to the right of the removed cell slides over and we're all good.
Another problem to get over: making sure your gesture recognizer and the collection view's one play nice together. Thankfully, that's not too tricky either.
[_collectionView.panGestureRecognizer requireGestureRecognizerToFail:pgr]; //where pgr is the recognizer you made for dragging the view off
This means in order for the collection view's pan gesture to do its thing, your one has to fail. So you'll want to set yours up so that it only works when panning up and down, and let the collection view still do its thing for left to right pans. In your gesture recognizers's delegate, implement the following method which simply checks if you're moving more on the x-axis or y-axis.
-(BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
{
CGPoint translation =[gestureRecognizer translationInView:self.view];
return(translation.x * translation.x > translation.y * translation.y);
}
I was looking for this functionality and using #mbehan suggestion i faked this functionality using UICollectionView.
What i did is i added a view of smaller size on a collection cell(Transparent background) and added a single pan gesture on CollectionView(not on each cell) then on pan gesture i move the view and it looks like the cell is moving. After view reaches some point i first hide it and then deletes the collection view cell.
Cell Hierarchy : collectionViewCell -> View(tag value==2) -> UILabel(tag Value == 1)
Label is just used for placeholder purpose.
i am posting my code below:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = (UICollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:#"Cards" forIndexPath:indexPath];
UILabel *lblNumber = (UILabel*)[cell.contentView viewWithTag:1];
UIView *viewTouch = (UIView*)[cell.contentView viewWithTag:2];
[viewTouch setHidden:NO];
[lblNumber setText:arrCards[indexPath.row]];
return cell;
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
return UIEdgeInsetsMake(0, 50, 0, 30);
}
-(BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
{
if([gestureRecognizer isEqual:panGesture]) {
CGPoint point = [(UIPanGestureRecognizer*)gestureRecognizer translationInView:collectionView_];
if(point.x != 0) { //adjust this condition if you want some leniency on the X axis
//The translation was on the X axis, i.e. right/left,
//so this gesture recognizer shouldn't do anything about it
return NO;
}
}
return YES;
}
- (IBAction)panGestureCalled:(UIPanGestureRecognizer *)sender {
yFromCenter = [sender translationInView:collectionView_].y; //%%% positive for up, negative for down
UIView *view = sender.view;
CGPoint location = [view.superview convertPoint:view.center toView:collectionView_];
NSIndexPath *indexPath = [collectionView_ indexPathForItemAtPoint:location];
UICollectionViewCell *cell = [collectionView_ cellForItemAtIndexPath:indexPath];
UIView *touchView = (UIView*)[cell.contentView viewWithTag:2];
switch (sender.state) {
case UIGestureRecognizerStateBegan:{
originalPoint = touchView.center;
break;
};
case UIGestureRecognizerStateChanged:{
touchView.center = CGPointMake(originalPoint.x , originalPoint.y + yFromCenter);
break;
};
//%%% let go of the card
case UIGestureRecognizerStateEnded: {
CGFloat velocityY = (0.2*[(UIPanGestureRecognizer*)sender velocityInView:collectionView_].y);
if (velocityY < -30 && yFromCenter<0) {
[self hideView:touchView withDuration:0.2 andIndexPath:indexPath];
}else if ((yFromCenter< 0 && yFromCenter > -200) || yFromCenter > 0){
CGFloat animationDuration = (ABS(velocityY)*.0002)+.2;
[self resettleViewToOriginalPosition:touchView andDuration:animationDuration];
}else
[self hideView:touchView withDuration:0.2 andIndexPath:indexPath];
};
break;
case UIGestureRecognizerStatePossible:break;
case UIGestureRecognizerStateCancelled:break;
case UIGestureRecognizerStateFailed:break;
}
}
-(void)resettleViewToOriginalPosition:(UIView*)view andDuration:(float)duration{
[UIView animateWithDuration:duration
delay:0.0f
options: UIViewAnimationOptionCurveEaseOut
animations:^
{
[view setCenter:originalPoint];
}
completion:^(BOOL finished)
{
}];
}
- (void)hideView:(UIView*)view withDuration:(float)duration andIndexPath:(NSIndexPath*)indexPath
{
[UIView animateWithDuration:duration
delay:0.0f
options: UIViewAnimationOptionCurveEaseOut
animations:^
{
CGRect frame = view.frame;
frame.origin.y = -300;
view.frame = frame;
}
completion:^(BOOL finished)
{
[view setHidden:YES];
CGRect frame = view.frame;
frame.origin.y = 39;
view.frame = frame;
NSLog(#"View is hidden.");
[arrCards removeObjectAtIndex:indexPath.row];
[collectionView_ performBatchUpdates:^{
[collectionView_ deleteItemsAtIndexPaths:#[indexPath]];
} completion:^(BOOL finished) {
// you might want to remove the data from the data source here so the view doesn't come back to life when the collection view is reloaded.
}];
}];
}
and keep pagingEnabled of CollectionView to NO and then it should be good to go.

How do I detect a touch event on a moving UIImageView?

When researching "How do I detect a touch event on a moving UIImageView?" I've come across several answers and I tried to implement them to my app. Nothing I've come across seems to work. I'll explain what I'm trying to do then post my code. Any thoughts, suggestions, comments or answers are appreciated!
My app has several cards floating across the screen from left to right. These cards are various colors and the object of the game is the drag the cards down to their similarly colored corresponding container. If the user doesn't touch and drag the cards fast enough, the cards will simply drift off the screen and points will be lost. The more cards contained in the correct containers, the better the score.
I've written code using core animation to have my cards float from the left to right. This works. However when attempting to touch a card and drag it toward it's container, it isn't correctly detecting that I'm touching the UIImageView of the card.
To test if my I'm properly implementing the code to move a card, I've also written some code allows movement for a non-moving card. In this case my touch is being detected and acting accordingly.
Why can I only interact with stationary cards? After researching this quite a bit it seems that the code:
options:UIViewAnimationOptionAllowUserInteraction
is the key ingredient to get my moving UIImages to be detected. However I tried this doesn't seem to have any effect.
I another key thing that I may be doing wrong is not properly utilizing the correct presentation layer. I've added code like this to my project and I also only works on non-moving objects:
UITouch *t = [touches anyObject];
UIView *myTouchedView = [t view];
CGPoint thePoint = [t locationInView:self.view];
if([_card.layer.presentationLayer hitTest:thePoint])
{
NSLog(#"You touched a Card!");
}
else{
NSLog(#"backgound touched");
}
After trying these types of things I'm getting stuck. Here is my code to understand this a bit more completely:
#import "RBViewController.h"
#import <QuartzCore/QuartzCore.h>
#interface RBViewController ()
#property (nonatomic, strong) UIImageView *card;
#end
#implementation RBViewController
- (void)viewDidLoad
{
srand(time (NULL)); // will be used for random colors, drift speeds, and locations of cards
[super viewDidLoad];
[self setOutFirstCardSet]; // this sends out 4 floating cards across the screen
// the following creates a non-moving image that I can move.
_card = [[UIImageView alloc] initWithFrame:CGRectMake(400,400,100,100)];
_card.image = [UIImage imageNamed:#"goodguyPINK.png"];
_card.userInteractionEnabled = YES;
[self.view addSubview:_card];
}
the following method sends out cards from a random location on the left side of the screen and uses core animation to drift the card across the screen. Notice the color of the card and the speed of the drift will be randomly generated as well.
-(void) setOutFirstCardSet
{
for(int i=1; i < 5; i++) // sends out 4 shapes
{
CGRect cardFramei;
int startingLocation = rand() % 325;
CGRect cardOrigini = CGRectMake(-100,startingLocation + 37, 92, 87);
cardFramei.size = CGSizeMake(92, 87);
CGPoint origini;
origini.y = startingLocation + 37;
origini.x = 1200;
cardFramei.origin = origini;
_card.userInteractionEnabled = YES;
_card = [[UIImageView alloc] initWithFrame:cardOrigini];
int randomColor = rand() % 7;
if(randomColor == 0)
{
_card.image = [UIImage imageNamed:#"goodguy.png"];
}
else if (randomColor == 1)
{
_card.image = [UIImage imageNamed:#"goodguyPINK.png"];
}
else if (randomColor == 2)
{
_card.image = [UIImage imageNamed:#"goodGuyPURPLE.png"];
}
else if (randomColor == 3)
{
_card.image = [UIImage imageNamed:#"goodGuyORANGE.png"];
}
else if (randomColor == 4)
{
_card.image = [UIImage imageNamed:#"goodGuyLightPINK.png"];
}
else if (randomColor == 5)
{
_card.image = [UIImage imageNamed:#"goodGuyBLUE.png"];
}
else if (randomColor == 6)
{
_card.image = [UIImage imageNamed:#"goodGuyGREEN.png"];
}
_card.userInteractionEnabled = YES; // this is also written in my viewDidLoad method
[[_card.layer presentationLayer] hitTest:origini]; // not really sure what this does
[self.view addSubview:_card];
int randomSpeed = rand() % 20;
int randomDelay = rand() % 2;
[UIView animateWithDuration:randomSpeed + 10
delay: randomDelay + 4
options:UIViewAnimationOptionAllowUserInteraction // here is the method that I thought would allow me to interact with the moving cards. Not sure why I can't
animations: ^{
_card.frame = cardFramei;
}
completion:NULL];
}
}
notice the following method is where I put CALayer and hit test information. I'm not sure if I'm doing this correctly.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *t = [touches anyObject];
UIView *myTouchedView = [t view];
CGPoint thePoint = [t locationInView:self.view];
thePoint = [self.view.layer convertPoint:thePoint toLayer:self.view.layer.superlayer];
CALayer *theLayer = [self.view.layer hitTest:thePoint];
if([_card.layer.presentationLayer hitTest:thePoint])
{
NSLog(#"You touched a Shape!"); // This only logs when I touch a non-moving shape
}
else{
NSLog(#"backgound touched"); // this logs when I touch the background or an moving shape.
}
if(myTouchedView == _card)
{
NSLog(#"Touched a card");
_boolHasCard = YES;
}
else
{
NSLog(#"Didn't touch a card");
_boolHasCard = NO;
}
}
I want the following method to work on moving shapes. It only works on non-moving shapes. Many answers say to have the touch ask which class the card is from. As of now all my cards on of the same class (the viewController class). When trying to have the cards be their own class, I was having trouble having that view appear on my main background controller. Must I have various cards be from different classes for this to work, or can I have it work without needing to do so?
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[event allTouches] anyObject];
if([touch view]==self.card)
{
CGPoint location = [touch locationInView:self.view];
self.card.center=location;
}
}
This next method resets the movement of a card if the user starts moving it and then lifts up on it.
-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
if(_boolHasCard == YES)
{
[UIView animateWithDuration:3
delay: 0
options:UIViewAnimationOptionAllowUserInteraction
animations: ^{
CGRect newCardOrigin = CGRectMake(1200,_card.center.y - 92/2, 92, 87);
_card.frame = newCardOrigin;
}
completion:NULL];
}
}
#end
The short answer is, you can't.
Core Animation does not actually move the objects along the animation path. They move the presentation layer of the object's layer.
The moment the animation begins, the system thinks the object is at it's destination.
There is no way around this if you want to use Core Animation.
You have a couple of choices.
You can set up a CADisplayLink on your view controller and roll your own animation, where you move the center of your views by a small amount on each call to the display link. This might lead to poor performance and jerky animation if you're animating a lot of objects however.
You can add a gesture recognizer to the parent view that contains all your animations, and then use layer hit testing on the paren't view's presentation view to figure out which animating layer got tapped, then fetch that layer's delegate, which will be the view you are animating. I have a project on github that shows how to do this second technique. It only detects taps on a single moving view, but it will show you the basics: Core Animation demo project on github.
(up-votes always appreciated if you find this post helpful)
It looks to me that your problem is really with just an incomplete understanding of how to convert a point between coordinate spaces. This code works exactly as expected:
- (void)viewDidLoad
{
[super viewDidLoad];
CGPoint endPoint = CGPointMake([[self view] bounds].size.width,
[[self view] bounds].size.height);
CABasicAnimation *animation = [CABasicAnimation
animationWithKeyPath:#"position"];
animation.fromValue = [NSValue valueWithCGPoint:[[_imageView layer] position]];
animation.toValue = [NSValue valueWithCGPoint:endPoint];
animation.duration = 30.0f;
[[_imageView layer] addAnimation:animation forKey:#"position"];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *t = [touches anyObject];
CGPoint thePoint = [t locationInView:self.view];
thePoint = [[_imageView layer] convertPoint:thePoint
toLayer:[[self view] layer]];
if([[_imageView layer].presentationLayer hitTest:thePoint])
{
NSLog(#"You touched a Shape!");
}
else{
NSLog(#"backgound touched");
}
}
Notice the line in particular:
thePoint = [[_imageView layer] convertPoint:thePoint
toLayer:[[self view] layer]];
When I tap on the layer image view while it's animating, I get "You touched a Shape!" in the console window and I get "background touched" when I tap around it. That's what you're wanting right?
Here's a sample project on Github
UPDATE
To help with your follow up question in the comments, I've written the touchesBegan code a little differently. Imagine that you've add all of your image views to an array (cleverly named imageViews) when you create them. You would alter your code to look something like this:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *t = [touches anyObject];
CGPoint thePoint = [t locationInView:self.view];
for (UIImageView *imageView in [self imageViews]) {
thePoint = [[imageView layer] convertPoint:thePoint
toLayer:[[self view] layer]];
if([[imageView layer].presentationLayer hitTest:thePoint]) {
NSLog(#"Found it!!");
break; // No need to keep iterating, we've found it
} else{
NSLog(#"Not this one!");
}
}
}
I'm not sure how expensive this is, so you may have to profile it, but it should do what you're expecting.

How do I use two actions in a UIPanGestureRecognizer?

I am working with two subviews. Each will be unique and have it's own "action".
Subview 1 = User can drag around the view, rotate, and zoom it
Subview 2 = When user moves finger across their screen an image is added at each point their finger touches.
I have both of these completed by using UIPanGestureRecognizer. My question is, how can I separate these two actions? I want to be able to add one subview, do what is required, and then when I add the other subview, prevent the previous actions from occurring.
Here is what I have tried, this is done in my panGesture method:
for (UIView * subview in imageView.subviews)
{
if ([subview isKindOfClass:[UIImageView class]])
{
if (subview == _aImageView)
{
CGPoint translation = [panRecognizer translationInView:self.view];
CGPoint imageViewPosition = _aImageView.center;
imageViewPosition.x += translation.x;
imageViewPosition.y += translation.y;
_aImageView.center = imageViewPosition;
[panRecognizer setTranslation:CGPointZero inView:self.view];
}
else if (subview == _bImageView)
{
currentTouch = [panRecognizer locationInView:self.view];
CGFloat distance = [self distanceFromPoint:currentTouch ToPoint:prev_touchPoint];
accumulatedDistance += distance;
CGFloat fixedDistance = 60;
if ([self distanceFromPoint:currentTouch ToPoint:prev_touchPoint] > fixedDistance)
{
[self addbImage];
prev_touchPoint = currentTouch;
}
}
}
}
If you want different gesture recognition in two different views, put separate recognizers on each view.
Usually, you want to have your view controller own and manage gesture recognizers, e.g.
- (void)viewDidLoad {
self.panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
self.panGesture.delegate = self;
[self.viewX addGestureRecognizer:self.panGesture];
// repeat with other recognisers...
}
Note that setting your controller as delegate of the gestureRecognizer is important: this enables you to handle the following delegate method from the view controller (which was the main question):
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
// handle your logic, which gestureRecognizer should proceed...
return NO;
}
The handler method is the same is this example, but you can set up your own handlers as you like:
- (void)handleGesture:(UIGestureRecognizer*)gestureRecognizer {
// handle gesture (usually sorted by state), e.g.
// if(gesture.state == UIGestureRecognizerStateEnded) { ... }
}

detect long press on UINavigationItem's back button

I want to add functionality to my back buttons through my UINavigationController-based app where long-pressing the back button will pop to root. However, I can't figure out where to attach the gesture recognizer. Do I subclass UINavigationBar and try and detect if the long press is in the left button region?
I've heard of people adding similar functionality before. Anyone have any ideas?
I know this question is old, but I came up with a solution. Instead of trying to add the gesture recognizer to the button itself (which would be ideal), I added it to the self.navigationController.navigationBar and then in the action method, use the locationInView to see if I'm over the back button. I wasn't entirely sure about how to identify the back button precisely, so I'm clumsily just grabbing the the first subview with an x coordinate less than some arbitrary value, but it seems promising. If someone has a better way to identify the frame of the back button, let me know.
- (void)longPress:(UILongPressGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
// set a default rectangle in case we don't find the back button for some reason
CGRect rect = CGRectMake(0, 0, 100, 40);
// iterate through the subviews looking for something that looks like it might be the right location to be the back button
for (UIView *subview in self.navigationController.navigationBar.subviews)
{
if (subview.frame.origin.x < 30)
{
rect = subview.frame;
break;
}
}
// ok, let's get the point of the long press
CGPoint longPressPoint = [sender locationInView:self.navigationController.navigationBar];
// if the long press point in the rectangle then do whatever
if (CGRectContainsPoint(rect, longPressPoint))
[self doWhatever];
}
}
- (void)addLongPressGesture
{
if (NSClassFromString(#"UILongPressGestureRecognizer"))
{
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPress:)];
[self.navigationController.navigationBar addGestureRecognizer:longPress];
[longPress release];
}
}
I believe UIGestureRecognizers can only be added to UIViews and subclasses of UIViews.
http://developer.apple.com/library/ios/#documentation/uikit/reference/UIView_Class/UIView/UIView.html
The back button is a UIBarButtonItem that descends from NSObject. Therefore, you won't be able to attach a gesture recognizer to a standard back button using
UILongPressGestureRecognizer *longPressGesture =
[[[UILongPressGestureRecognizer alloc]
initWithTarget:self action:#selector(longPress:)] autorelease];
[self.navigationItem.backBarButtonItem addGestureRecognizer:longPressGesture];
You can however add a custom view to a UIBarButtonItem. A custom view could just as easily be a UIView, UIButton, UILabel, etc.
Example:
UIView *myTransparentGestureView = [[UIView alloc] initWithFrame:CGRectMake(0,0,40,30)];
[myTransparentGestureView addGestureRecognizer:longPressGesture];
[self.navigationItem.backBarButtonItem setCustomView:myTransparentGestureView];
// Or you could set it like this
// self.navigationItem.backBarButtonItem.customView = myTransparentGestureView;
[myTransparentGestureView release];
You have to be careful however, since setting properties on backBarButtonItem applies to the next view that you push. So if you have view A that pushes to view B and you want the gesture to be recognized when you tap back in view B. You must set it up in view A.
I followed a slightly different path, figured I'd share it. The above answers are fine, but really, if the long press is in the leading 1/3 of the nav bar, that's good enough for me:
- (void)longPress:(UILongPressGestureRecognizer *)gr
{
NSLog(#"longPress:");
UINavigationBar *navBar = [self navigationBar];
CGFloat height = navBar.bounds.size.height;
CGPoint pt = [gr locationOfTouch:0 inView:navBar];
//NSLog(#"PT=%# height=%f", NSStringFromCGPoint(pt), height);
if(CGRectContainsPoint(CGRectMake(0,0,100,height), pt)) {
[self popToViewController:self.viewControllers[0] animated:YES];
}
}
Here's my solution:
In appDelegate (the "owner" of the nav bar in my app), In applicationDidFinishLaunchingWithOptions:
Get the nav bar view and add the gesture recognizer to the whole view:
// Get the nav bar view
UINavigationBar *myNavBar = nil;
for (UIView *view in [self.window.rootViewController.view subviews]) {
if ([view isKindOfClass:[UINavigationBar class]]) {
NSLog(#"Found Nav Bar!!!");
myNavBar = (UINavigationBar *)view;
}
}
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self
action:#selector(backButtonLongPress:)];
[myNavBar addGestureRecognizer:longPress];
NSLog(#"Gesture Recognizer Added.");
Then in appDelegate, in -(void) backButtonLongPress:(id) sender
Check to see if the gesture occurs within the frame of the back button:
if ([sender state] == UIGestureRecognizerStateBegan) {
// Get the nav bar view
UINavigationBar *myNavBar = nil;
for (UIView *view in [self.window.rootViewController.view subviews]) {
if ([view isKindOfClass:[UINavigationBar class]]) {
NSLog(#"Found Nav Bar!!!");
myNavBar = (UINavigationBar *)view;
}
}
// Get the back button view
UIView *backButtonView = nil;
for (UIView *view in [myNavBar subviews]) {
if ([[[view class] description] isEqualToString:#"UINavigationItemButtonView"]) {
backButtonView = view;
NSLog(#"Found It: %#", backButtonView);
NSLog(#"Back Button View Frame: %f, %f; %f, %f", backButtonView.frame.origin.x, backButtonView.frame.origin.y, backButtonView.frame.size.width, backButtonView.frame.size.height);
}
}
CGPoint longPressPoint = [sender locationInView:myNavBar];
NSLog(#"Touch is in back button: %#", CGRectContainsPoint(backButtonView.frame, longPressPoint) ? #"YES" : #"NO");
if (CGRectContainsPoint(backButtonView.frame, longPressPoint)) {
// Place your action here
}
// Do nothing if outside the back button frame
}