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.

Blazej SLEBODA
  • 6,503
  • 3
  • 35
  • 69
Keith Norman
  • 475
  • 4
  • 11
  • 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. – KudoCC Jan 24 '14 at 08:11
  • Yep, it seems like it should work, right? Thanks for checking it out and confirming that it seems weird to you as well :) – Keith Norman Jan 24 '14 at 08:16
  • Yes, I download your project and expect it should work but failed. I also confused about why it skips the `TableCell`. – KudoCC Jan 24 '14 at 08:23
  • So your aim is you just want trigger the customEventFired: method at TableCell. right? – Vaisakh Jan 24 '14 at 08:37
  • I want to have the event triggered in a subview but be captured by the TableCell. – Keith Norman Jan 24 '14 at 08:38
  • 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. – Fábio Oliveira Jul 23 '15 at 15:41

5 Answers5

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)
}
Blazej SLEBODA
  • 6,503
  • 3
  • 35
  • 69
nschmidt
  • 2,358
  • 16
  • 22
  • 1
    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? – Ole Begemann Jun 21 '14 at 18:10
  • 1
    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. – nschmidt Jun 21 '14 at 19:13
  • Interesting, thanks for the answer! I updated my sample project at https://github.com/keithnorm/ResponderChainTest to show that this does indeed work. – Keith Norman Jun 24 '14 at 21:43
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.

Keith Norman
  • 475
  • 4
  • 11
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

Shankar BS
  • 8,284
  • 6
  • 39
  • 53
  • 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. – Keith Norman Jan 24 '14 at 08:06
  • 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 subview – Shankar BS Jan 24 '14 at 09:29
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)];
Vaisakh
  • 1,088
  • 1
  • 7
  • 14
  • 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. – Keith Norman Jan 24 '14 at 08:43
-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];
Sebastian Boldt
  • 4,947
  • 9
  • 46
  • 60
  • 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. – Blazej SLEBODA Sep 05 '19 at 15:59
  • @Adobels Thats actually wrong information you are propagating. Many Key Objects are also responders including the UIApplication Object, UIViewcontrollers and UIViews. – Sebastian Boldt Sep 05 '19 at 16:18
  • you confuse a next responder with a first responder. – Blazej SLEBODA Sep 05 '19 at 16:28