Search code examples
iosuiviewanimationuitapgesturerecognizer

UITapGestureRecognizer not responding on animated subview


I have a simple program that creates a subview and animates it across the screen.

As part of this program, I would like to add functionality when the subview is tapped. I am using the following method to create the subview, add the UITapGestureRecognizer and then animate the subview:

int randomName = arc4random() % ([pieceNames count] - 1);
int animationDuration = arc4random() %  5 + 5 ;
NSString *randomPiece = [pieceNames objectAtIndex:randomName];

float yStart = arc4random() % 650;
float yEnd = arc4random() % 650;

UIView *piece = [[PieceView alloc]initWithFrame:CGRectMake(100.0, yStart, 75.0, 75.0)];
[piece setValue:randomPiece forKey:@"name"];

UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc]initWithTarget:self
                                                                            action:@selector(handleTouch:)];
[piece addGestureRecognizer:recognizer];

[[self view] addSubview:piece];

[UIView animateWithDuration:animationDuration
                      delay:0.0
                    options:UIViewAnimationOptionAllowUserInteraction
                 animations:^(void){
                     piece.center = CGPointMake(950.0, yEnd);
                 } completion:^(BOOL done){
                     [piece removeFromSuperview];
                 }];

Here is the code that handles the tap:

PieceView *pv = (PieceView *) recognizer.view;
NSLog(@"%@ was tapped", pv.name);

What happens is when a PieceView is touched the program does not respond. However, if I remove the animation block then the program responds to the tap.

Why does the UITapGestureRecognizer fail to respond to PieceView when it is animated?


Solution

  • I struggled with this same problem, and it boils down to this: the animated view only ever is in two places: the starting position, and the ending position. Core Animation simply renders the view's layer in interpolated positions between the start and end points over a period of time.

    It's almost like when you look to the stars and realize that what you see is not actually what is happening right now. :)

    Luckily, the solution is pretty simple. You can put a tap recognizer on the superview and then inspect your animated view's presentationLayer (which does give you an accurate frame at any point in time) to determine if your tap is a hit or not.

    I've built a simple UIViewController that demonstrates both the problem and solution:

    #import <UIKit/UIKit.h>
    
    @interface MSMViewController : UIViewController
    
    @end
    

    And the implementation:

    #import "MSMViewController.h"
    
    @interface MSMViewController ()
    @property (nonatomic, strong) UIView *animatedView;
    @end
    
    @implementation MSMViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        CGRect startFrame = CGRectMake(125, 0, 70, 70);
        CGRect endFrame = CGRectMake(125, 400, 70, 70);
    
        // draw a box to show where the animated view begins
        UIView *startOutlineView = [[UIView alloc] initWithFrame:startFrame];
        startOutlineView.layer.borderColor = [UIColor blueColor].CGColor;
        startOutlineView.layer.borderWidth = 1;
        [self.view addSubview:startOutlineView];
    
        // draw a box to show where the animated view ends
        UIView *endOutlineView = [[UIView alloc] initWithFrame:endFrame];
        endOutlineView.layer.borderColor = [UIColor blueColor].CGColor;
        endOutlineView.layer.borderWidth = 1;
        [self.view addSubview:endOutlineView];
    
        self.animatedView = [[UIView alloc] initWithFrame:startFrame];
        self.animatedView.backgroundColor = [UIColor yellowColor];
        [self.view addSubview:self.animatedView];
    
        [UIView animateWithDuration:10 delay:2 options:UIViewAnimationOptionAllowUserInteraction animations:^{
    
            self.animatedView.frame = endFrame;
    
        } completion:nil];
    
        // this gesture recognizer will only work in the space where endOutlintView is
        UITapGestureRecognizer *boxTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(boxTap:)];
        [self.animatedView addGestureRecognizer:boxTap];
    
        // this one will work
        UITapGestureRecognizer *superviewTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(superviewTap:)];
        [self.view addGestureRecognizer:superviewTap];
    
    }
    
    - (void)boxTap:(UITapGestureRecognizer *)tap {
        NSLog(@"tap. view is at %@", NSStringFromCGPoint(self.animatedView.frame.origin));
    }
    
    - (void)superviewTap:(UITapGestureRecognizer *)tap {
        CGRect boxFrame = [self.animatedView.layer.presentationLayer frame];
        if (CGRectContainsPoint(boxFrame, [tap locationInView:self.view])) {
            NSLog(@"we tapped the box!");
        }
    }
    
    @end