18

According to the iOS documentation, the responder chain is used to pass touch events "up the chain". It's also used for actions generated by controls. Fine.

What I really would like to do is send a custom event "up the chain". The first responder to pick up on the event will handle it. This seems like a pretty common pattern, but I can't find any good explanation on how to do it the "iOS/Cocoa way".

Since the responder chain is exactly what I need, I came up with a solution like this:

// some event happened in my view that 
// I want to turn into a custom event and pass it "up":

UIResponder *responder = [self nextResponder];

while (responder) {

   if ([responder conformsToProtocol:@protocol(ItemSelectedDelegate)]) {
       [responder itemSelected:someItem];
       break;
   } 

   responder = [responder nextResponder];
}

This works perfectly, but I have a feeling that there should be other ways of handling this. Walking the chain manually this way doesn't seem very... nice.

Note that notifications are not a good solution here, because I only want the objects in the view hierarchy to be involved, and notifications are global.

What's the best way of handling this in iOS (and Cocoa for that matter)?

EDIT:

What do I want to accomplish?

I have a view controller, which has a view, which has subviews etc... Several of the subviews are of a specific type that show an item from the database. When the user taps this view, a signal should be sent to the controller to navigate to a detail page of this item.

The view that handles the tap is several levels below the main view in the view hierarchy. I have to tell the controller (or in some cases a specific subview "up the chain") that an item was selected.

Listening to notifications would be an option, but I don't like that solution because selecting an item is not a global event. It's strictly tied to the current view controller.

twe4ked
  • 2,832
  • 21
  • 24
Philippe Leybaert
  • 168,566
  • 31
  • 210
  • 223
  • "...I have a feeling that there should be other ways of handling this..." There may be, but it's hard to tell without knowing exactly why you want to pass a custom event up the chain. What's your end goal? – Joshua Nozzi Oct 05 '10 at 13:54
  • See my added explanation in the question. – Philippe Leybaert Oct 05 '10 at 14:10
  • Re this five year old question ... here's the beautiful modern Swift way to do certain tasks like this http://stackoverflow.com/a/37515358/294884 – Fattie Jun 03 '16 at 14:18
  • And even better ... https://blog.veloxdb.com/2016/05/12/bubbling-events-using-uiresponder-in-swift/ – Fattie Jun 03 '16 at 14:20
  • Using showDetailViewController is the correct way to accomplish this which makes use of canPerformAction and targetViewControllerForAction. – malhal Sep 03 '19 at 13:15

3 Answers3

16

UIApplication has a method for just this purpose, as does its Cocoa cousin. You can replace all of that code in your question with one message.

Peter Hosey
  • 95,783
  • 15
  • 211
  • 370
  • 2
    I have read this, and somehow I didn't notice the line in the documentation that says *"If target is nil, the application sends the message to the first responder, from whence it progresses up the responder chain until it is handled."*. Thanks a lot for this answer! Very helpful! – Philippe Leybaert Oct 06 '10 at 08:45
  • 1
    Followup question: This seems to be a great solution, but it assumes you're always working with UIEvent objects. UIEvent objects are very limited in scope (only touches, gestures, etc...). What if I want a higher-level custom event like "item x selected", or "item x deleted"? That wouldn't work with "sendAction" – Philippe Leybaert Oct 06 '10 at 11:09
  • while you could indeed replace all the code with 1 message, you lose the ability to send some data or parameter along? As sendAction:to:from:forEvent: does not support this, unless you would misuse the from or event parameter. You could grab the data from the from parameter yourself, but I'd like the data or context to be bundled and independent. – ynnckcmprnl Oct 06 '10 at 11:14
  • Misusing the from: parameter there isn't very practical, because it is being assumed to be a UIControl subclass. – Martijn Thé Sep 12 '11 at 10:17
  • 1
    Rather than linking to the overall docs, could you please provide a specific answer to the question. Which is the method you recommend? – cleverbit Sep 25 '14 at 13:41
  • @richarddas: I've tweaked the links. They are both addressed to the specific methods in question, although (due to a frustrating implementation detail in the previous generation of Apple's documentation framing gizmo) you would have to look at the URL for the Cocoa one at the moment. I'll save you the effort: The Cocoa link goes to `sendAction:to:from:`. – Peter Hosey Sep 27 '14 at 03:26
  • Aha, cool thanks! I've implemented `sendAction:to:from` in my app and it's working great. But yeah, walking the Responder Chain can very easily lead to code that's hard to manage, since you can only pass params via the `from:` object, which is hacky. – cleverbit Sep 30 '14 at 10:16
12

You're pretty close. What would be more standard is something like this:

@implementation NSResponder (MyViewController)
- (void)itemSelected:(id)someItem
{
    [[self nextResponder] itemSelected:someItem];
}
@end

That's generally how events get passed up the chain by default. Then in the right controller, override that method to instead take a custom action.

This may not be the right pattern for what you want to achieve, but it is a good way to pass messages up the responder chain.

Mike Abdullah
  • 14,933
  • 2
  • 50
  • 75
5

Peter's solution works if you are sure that the first responder is set correctly. If you want more control over which object is notified about events, you should use targetForAction:withSender: instead.

This allows you to specify the first view that should be able to handle the event, and from there it will crawl up the responder chain until it finds an object that can handle the message.

Here is a fully documented function you can use:

@interface ABCResponderChainHelper
/*!
 Sends an action message identified by selector to a specified target's responder chain.
 @param action 
    A selector identifying an action method. See the remarks for information on the permitted selector forms.
 @param target 
    The object to receive the action message. If @p target cannot invoke the action, it passes the request up the responder chain.
 @param sender
    The object that is sending the action message.
 @param userInfo
    The user info for the action. This parameter may be @c nil.
 @return
    @c YES if a responder object handled the action message, @c NO if no object in the responder chain handled the message.
 @remarks
    This method pushes two parameters when calling the target. This design enables the action selector to be one of the following:
 @code
 - (void)action
 - (void)action:(id)sender
 - (void)action:(id)sender userInfo:(id)userInfo@endcode
*/
+ (BOOL)sendResponderChainAction:(SEL)action to:(UIResponder *)target from:(id)sender withUserInfo:(id)userInfo;
@end

Implementation:

@implementation ABCResponderChainHelper
+ (BOOL)sendResponderChainAction:(SEL)action to:(UIResponder *)target from:(id)sender withUserInfo:(id)userInfo {
    target = [target targetForAction:action withSender:sender];
    if (!target) {
        return NO;
    }

    NSMethodSignature *signature = [target methodSignatureForSelector:action];
    const NSInteger hiddenArgumentCount = 2; // self and _cmd
    NSInteger argumentCount = [signature numberOfArguments] - hiddenArgumentCount;
    switch (argumentCount) {
        case 0:
            SuppressPerformSelectorLeakWarning([target performSelector:action]);
            break;
        case 1:
            SuppressPerformSelectorLeakWarning([target performSelector:action withObject:sender]);
            break;
        case 2:
            SuppressPerformSelectorLeakWarning([target performSelector:action withObject:sender withObject:userInfo]);
            break;
        default:
            NSAssert(NO, @"Invalid number of arguments.");
            break;
    }

    return YES;
}
@end

Note: this uses the SuppressPerformSelectorLeakWarning macro.

This works great in a UITableView. Imagine you have a gesture recognizer on the cell, and you need to inform the view controller of the action that was performed. Instead of having to create a delegate for the cell, and then forwarding the message to the delegate, you can use the responder chain instead.

Sample usage (sender):

// in table view cell class:
- (void)longPressGesture:(UILongPressGestureRecognizer *)recognizer {
    // ...
    [ABCResponderChainHelper sendResponderChainAction:@selector(longPressCell:) to:self from:self withUserInfo:nil];
}

Here self refers to the cell itself. The responder chain ensures that the method is first sent to the UITableViewCell, then the UITableView, and eventually to the UIViewController.

Sample usage (receiver):

#pragma mark - Custom Table View Cell Responder Chain Messages
- (void)longPressCell:(UITableViewCell *)sender {
    // handle the long press
}

If you tried to do the same thing with sendAction:to:from:forEvent:, you have two problems:

  • In order for it to work with the responder chain, you must pass in nil to the to parameter, at which point it will start the messaging at the first responder rather than an object of your choice. Controlling the first responder can be cumbersome. With this method, you simply tell it at which object to start.
  • You can't easily pass a second argument with arbitrary data. You would need to subclass UIEvent and add a property such as userInfo and pass that along in the event argument; a clumsy solution in this case.
Community
  • 1
  • 1
Senseful
  • 86,719
  • 67
  • 308
  • 465