UITableViewCell skipped in responder chain
Asked Answered
B

5

13

I'm attempting to trigger an event in a subview of a UITableViewCell, and let it bubble up the responder chain and be handled by a custom UITableViewCell subclass.

Basically:

SomeView.m (which is a subview of the UITableViewCell)

[self.button addTarget:nil action:@selector(someAction:) events:UIControlEventTouchUpInside]

SomeCustomCell.m

- (void)someAction:(id)sender {
     NSLog(@"cool, the event bubbled up to the cell");
}

And to test why this wasn't working, I've added the someAction: method on the ViewController and the ViewController is the one that ends up handling the event that bubbles up from the table view cell subview, even though the Cell should handle it. I've checked that the Cell is on the responder chain and I've verified that any views on the responder chain both above and below the cell will respond to the event if they implement the someAction: method.

What the heck is going on here?

Here's a project that shows it https://github.com/keithnorm/ResponderChainTest Is this expected behavior somehow? I haven't found any documentation stating UITableViewCell's are treated any differently than other UIResponder's.

Bedivere answered 24/1, 2014 at 7:30 Comment(6)
It's weird! I found the responder chain is :ContentView->UITableViewCellContentView->UITableViewCellScrollView->TableCell->UITableViewWrapperView->UITableView->View->ViewController->UIWindow->UIApplication->AppDelegate. I find that by UIResponder * res = sender ; while (res) { res = [res nextResponder] ; NSLog(@"%@", [res class]) ; }. I will follow the question.Singband
Yep, it seems like it should work, right? Thanks for checking it out and confirming that it seems weird to you as well :)Bedivere
Yes, I download your project and expect it should work but failed. I also confused about why it skips the TableCell.Singband
So your aim is you just want trigger the customEventFired: method at TableCell. right?Salzburg
I want to have the event triggered in a subview but be captured by the TableCell.Bedivere
You would have to override canBecomeFirstResponder and return YES on your cell class in order to catch that action. Problem is, if you have more than one cell of the same type at the same time on the screen, which one should handle it (they are all potential first responders)? If it's a subclass then the action should be available, no need to use the responder chain.Cheadle
H
10

The cell seems to ask its table view for permission. To change that you can of course override

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
    return [self respondsToSelector:action];
}

Swift 3, 4, 5:

override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
    return self.responds(to: action)
}
Headpiece answered 21/6, 2014 at 17:17 Comment(3)
I can confirm that this works, although I am not sure “The cell seems to ask its table view for permission” is the right explanation for this behavior. What does the table view have to do with it? Is this documented somewhere?Lucullus
The table view is asking it's delegate -tableView:canPerformAction:forRowAtIndexPath:withSender:, but only if they are copy: or paste: :). That part is documented. And that is the reason why the responder chain doesn't work as expected: the cell hijacks the action to let the table view delegate perform menu validation.Headpiece
Interesting, thanks for the answer! I updated my sample project at github.com/keithnorm/ResponderChainTest to show that this does indeed work.Bedivere
B
1

I've concluded that this is either a bug or undocumented intended behavior. At any rate, I ended up brute force fixing it by responding to the event in a subview and then manually propagating the message up the responder chain. Something like:

- (void)customEventFired:(id)sender {
  UIResponder *nextResponder = self.nextResponder;
  while (nextResponder) {
    if ([nextResponder respondsToSelector:@selector(customEventFired:)]) {
      [nextResponder performSelector:@selector(customEventFired:) withObject:sender];
      break;
    }
    nextResponder = nextResponder.nextResponder;
  }
}

I've also updated my demo project to show how I'm using this "fix" https://github.com/keithnorm/ResponderChainTest.

I still welcome any other ideas if anyone else figures this out, but this is the best I've got for now.

Bedivere answered 24/1, 2014 at 22:3 Comment(0)
E
0

do like this

    @implementation ContentView

   // uncomment this to see event caught by the cell's subview

  - (id)initWithFrame:(CGRect)frame
 {
     self = [super initWithFrame:frame];
    if(self)
   {

    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button setTitle:@"Click" forState:UIControlStateNormal];
    [button setBackgroundColor:[UIColor blueColor]];
    [button addTarget:self action:@selector(customEventFired:) forControlEvents:UIControlEventTouchUpInside];
    button.frame = CGRectMake(4, 5, 100, 44);
    [self addSubview:button];
  }

    return self;
}

 - (void)customEventFired:(id)sender
{
     UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Event Triggered in cell subview" message:@"so far so good..." delegate:nil cancelButtonTitle:@"cancel" otherButtonTitles:nil, nil];
    [alertView show];
 }

@end

now customEventFired: method get called

Elysium answered 24/1, 2014 at 7:58 Comment(2)
Thanks but what I'm looking to do is specifically take advantage of the responder chain. My Cell does not have direct access to the button like your example does, and I want to keep it that way in order to keep those objects decoupled. Point is an event fired in a subview should, in theory, be sent to a superview. It's like event delegation / bubbling in JavaScript.Bedivere
oky sorry i did'n downloaded your code, just posted my answer directly, so i got now, wy dont u add the button in the contentView itself, then u get the responder to the cell's subviewElysium
S
0

You can change the code in View.m as

      [button addTarget:nil action:@selector(customEventFired:) forControlEvents:(1 << 24)];

to

      [button addTarget:cell action:@selector(customEventFired:) forControlEvents:(1 << 24)];
Salzburg answered 24/1, 2014 at 8:41 Comment(1)
I could if the subview had a reference to its parent cell, but that also breaks the decoupling I'm trying to maintain. A subview should pretty much never know about its superviews, IMO.Bedivere
P
-1

I think this one is the easiest solution. If you do not specify a target the event will automatically bubble up the responder chain.

[[UIApplication sharedApplication]sendAction:@selector(customAction:) to:nil from:self forEvent:UIEventTypeTouches];
Pentobarbital answered 3/6, 2015 at 13:43 Comment(3)
the QA is asking about an action forwarding from a UIButton to UITableViewCell. Your solutions will send an action to the first responder only if this one exist. The only UIKit componets which can become the first responder are UITextField and UITextView which is not a case in the question.Marigraph
@Adobels Thats actually wrong information you are propagating. Many Key Objects are also responders including the UIApplication Object, UIViewcontrollers and UIViews.Pentobarbital
you confuse a next responder with a first responder.Marigraph

© 2022 - 2024 — McMap. All rights reserved.