Search code examples
objective-ciosuikituigesturerecognizeruimenucontroller

UIMenuController not responding to first selection, only second


I have a view with a long press gesture recognizer in it:

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {            
        UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressDetected:)];
        [self addGestureRecognizer:longPress];
        [longPress release];
    }
    return self;
}

When the long press is detected, I want to show a UIMenuViewController above the view with a single action in it, and when that menu item is tapped I want to execute a block:

- (void)longPressDetected:(UILongPressGestureRecognizer *)recognizer {
    if (recognizer.state == UIGestureRecognizerStateBegan) {
        [self becomeFirstResponder];
        UIMenuController *menuController = [UIMenuController sharedMenuController];
        UIMenuItem *actionItem = [[UIMenuItem alloc] initWithTitle:@"Action" action:@selector(someActionSelector)];
        [menuController setMenuItems:[NSArray arrayWithObject:actionItem]];
        [actionItem release];

        [menuController setTargetRect:self.frame inView:self.superview];
        [menuController setMenuVisible:YES animated:YES];
    }
}

- (BOOL)canBecomeFirstResponder {
    return YES;
}

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
    if (action == @selector(copy:) || action == @selector(cut:) || action == @selector(delete:) || 
        action == @selector(paste:) || action == @selector(select:) || action == @selector(selectAll:)) {
        return NO;
    }
    else if (action == @selector(someActionSelector)) {
        return YES;
    }
    else {
        return [super canPerformAction:action withSender:sender];
    }
}

- (void)someActionSelector {
    if (self.actionBlock) {
        self.actionBlock();
    }
}

Problem is, this only works after the second long-press and tap combo. The first time I long-press on the view I see the menu, but tapping the menu does nothing. The second time I see the menu again, I tap it, then the block is executed.

Debugger shows that a breakpoint in someActionSelector is only reached on the second tap. Any idea why this is?


Solution

  • I figured it out. The view listening for a long press is contained inside a view that repositions some subviews when its frame is changed (by overriding setFrame:, which seems like a bad idea but I couldn't think of another way). So when the long press happened it triggered a layoutSubviews in the parent of the parent of the listening view, which set the frame of the parent of the listening view, which repositioned the listening view, which appears to break either the responder chain or deactivate the menu. The solution was to add a condition inside the overridden setFrame: to only trigger the layout if the frame actually changes, which it doesn't on the long press. I'm sure there's a better alternative for listening to frame changes that would avoid this problem entirely—feel free to suggest them in comments.