17

Apple is really funny. I mean, they say that this works:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch* touch = [touches anyObject];
    NSUInteger numTaps = [touch tapCount];
    if (numTaps < 2) {
        [self.nextResponder touchesBegan:touches withEvent:event];
   } else {
        [self handleDoubleTap:touch];
   }
}

I have a View Controller. Like you know, View Controllers inherit from UIResponder. That View Controller creates a MyView object that inherits from UIView, and adds it as a subview to it's own view.

So we have:

View Controller > has a View (automatically) > has a MyView (which is a UIView).

Now inside of MyView I put that code like above with a NSLog that prints "touched MyView". But I forward the event to the next responder, just like above. And inside the ViewController I have another touchesBegan method that just prints an NSLog a la "touched view controller".

Now guess what: When I touch the MyView, it prints "touched MyView". When I touch outside of MyView, which is the view of the VC then, I get a "touched view controller". So both work! But what doesn't work is forwarding the event. Because now, actually the next responder should be the view controller, since there's nothing else inbetween. But the event handling method of the VC is never called when I forward it.

%$&!§!!

Ideas?

Figured out weird stuff MyView's next responder is the view of the view controller. That makes sense, because MyView is a subview of that. But I didn't modify this UIView from the view controller. it's nothing custom. And it doesn't implement any touch event handling. Shouldn't the message get passed on to the view controller? How could I just let it pass? If I remove the event handling code in MyView, then the event arrives nicely in the view controller.

andersonvom
  • 11,701
  • 4
  • 35
  • 40

8 Answers8

5

According to a similar question, your method should work.

This leads me to think that your view's nextResponder is not actually the ViewController, as you suspect.

I would add a quick NSLog in your forwarding code to check what your nextResponder really points to:

if (numTaps < 2) {
    NSLog(@"nextResponder = %@", self.nextResponder);
    [self.nextResponder touchesBegan:touches withEvent:event];
}

You can also change your other NSLog messages so that they output type and address information:

NSLog(@"touched %@", self);

touched <UIView 0x12345678>

This should get you started on diagnosing the problem.

Community
  • 1
  • 1
e.James
  • 116,942
  • 41
  • 177
  • 214
  • Thanks eJames. The MyView tells me that next responder is exactly the view of the view controller. The memory adresses match perfect. however, this touchesBegan won't be called. but now I see that this method would be invoked on the view of the view controller, but not on the view controller itself. but then, wouldn't this event continue bubbling up to the view controller of the view doesn't handle it? or is that another special case because i send it manually? –  Jul 30 '09 at 20:06
  • Well, I suppose that makes sense. The default implementation of `touchesBegan:` does nothing, so once you pass it on to the next `UIView` object, it simply ends. Is it feasible to simply pass the touch event on to the viewController manually? – e.James Jul 30 '09 at 21:49
  • Have you tried calling `[super touchesBegan:...]` instead of `[self.nextResponder touchesBegan:...]` I have seen some people using that method, but I'm not sure what it does. – e.James Jul 30 '09 at 21:56
  • that should have the same effect, because views are in a hierarchy. the super is not the controller, but the view of the controller. –  Aug 01 '09 at 16:41
  • just noticed that I may be wrong with that. super != superview ;) a UIView's super should be UIResponder, so sending that to super makes no much sense I think. It would fall back on the view, maybe creating a loop? –  Aug 01 '09 at 16:43
  • That's true, but I think `UIResponder` may forward the `touchesBegan` method to the appropriate superview automatically? I'm afraid I don't know all of the details of how the responder chain works. – e.James Aug 01 '09 at 17:38
  • I've been having the same problem. Here's what i read in Apple's docs: If you implement a custom view to handle events or action messages, you should not forward the event or message to nextResponder directly to send it up the responder chain. Instead invoke the superclass implementation of the current event-handling method—let UIKit handle the traversal of the responder chain. – Spanky Nov 14 '09 at 04:31
  • 1
    http://developer.apple.com/IPhone/library/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/EventHandling/EventHandling.html#//apple%5Fref/doc/uid/TP40007072-CH9-SW2 – Spanky Nov 14 '09 at 04:33
  • Spanky's link is good. Also –touchesBegan:withEvent: docs say: To forward the message to the next responder, send the message to super (the superclass implementation); do not send the message directly to the next responder. – Olie Jun 08 '12 at 18:20
  • **EDIT**: Bah! Spanky's link =was= good, but Apple moved their documentation. The information seems to be here, now: https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/Introduction/Introduction.html – Olie Dec 01 '14 at 22:03
4

Had trouble with this, as my custom view was deeper in the view hierarchy. Instead, I climbed the responder chain until it finds a UIViewController;

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    // Pass to top of chain
    UIResponder *responder = self;
    while (responder.nextResponder != nil){
        responder = responder.nextResponder;
        if ([responder isKindOfClass:[UIViewController class]]) {
            // Got ViewController
            break;
        }
    }
    [responder touchesBegan:touches withEvent:event];
}
Autonomy
  • 402
  • 4
  • 13
3

If you look at the documentation of UIResponder the description says that by default:

UIView implements this method by returning the UIViewController object that manages it (if it has one) or its superview (if it doesn’t);

If your view isn't returning the view controller, it must be returning its superview then (as you figured out).

UIApplication actually has a method that is supposed to handle this use case exactly:

- (BOOL)sendAction:(SEL)action
                to:(id)target
              from:(id)sender
          forEvent:(UIEvent *)event

Since you don't have a reference to the target, you can work your way up the responder chain by setting target value as nil since according to the documentation:

If target is nil, the app sends the message to the first responder, from whence it progresses up the responder chain until it is handled.

You can of course get access to the UIApplication with its class method [UIApplication sharedApplication]

More info here: UIApplication documents

This is extra, but if you absolutely need access the view controller to do something a little more complex, you can work your way up the responder chain manually until you reach the view controller by calling the nextResponder method until it returns a view controller. For example:

UIResponder *responder = self;
while ([responder isKindOfClass:[UIView class]])
    responder = [responder nextResponder];

then you can just cast it as your view controller and proceed do whatever you wanted to do with it:

UIViewController *parentVC = (UIViewController *)responder

but note that kind of breaks the MVC pattern and likely means you're doing something in a "hacky" way

gadu
  • 1,816
  • 1
  • 17
  • 31
2

To reference a view controller, you need to use

[[self nextResponder] nextResponder]

because [self nextResponder] means a view controller's view, and [[self nextResponder] nextResponder] means the view controller itself, now you can get the touch event

inorganik
  • 24,255
  • 17
  • 90
  • 114
吴江伟
  • 39
  • 1
  • May well work in iOS9 & 10 but does not work in iOS11. You really need to climb the responder chain until you reach the viewController or tableViewController you're after. – ghr Jul 08 '17 at 13:17
2

Yes it should 100% work in common case. I've just tried with test project and everything seems to be ok.

I've created View-Based Application. Using Interface Builder put new UIView up to the present View. Then create new file with subclass of UIView and select just created class for my new view. (Interface Builder->Class identity->Class->MyViewClass)

Add touches handler functions both for MyViewClass and UIViewController.

//MyViewClass

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"myview touches");
[self.nextResponder touchesBegan:touches withEvent:event];
}

//ViewController

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {  
  NSLog(@"controller touches");
}   

I see both NSLogs when press MyViewClass. Did you use Interface Builder and XIB file when loading your ViewController or set view programatically with loadView function?

MikZ
  • 364
  • 2
  • 13
1

Today I found a better method to fix this or the question like this.

In yours UITableViewCell,you can add a delegate to listen the touch event, like this:

@protocol imageViewTouchDelegate

-(void)selectedFacialView:(NSInteger)row item:(NSInteger)rowIndex;

@end

@property(nonatomic,assign)id<imageViewTouchDelegate>delegate;

Then in yours UITableViewController:your can done like this:

@interface pressionTable : UITableViewController<facialViewDelegate>

and in the .m file you can implemate this delegate interface,like:

-(void)selectedFacialView:(NSInteger)row item:(NSInteger)rowIndex{
//TO DO WHAT YOU WANT}

NOTE:when you init the cell,you must set the delegate,otherwise the delegate is invalid,you can do like this

- (UITableViewCell *)tableView:(UITableView *)tableView 
     cellForRowAtIndexPath:(NSIndexPath *)indexPath {

static NSString *CellIdentifier = @"Cell";

pressionCell *cell = (pressionCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
    cell = [[pressionCell alloc]initWithFrame:CGRectMake(0, 0, 240, 40)];
}

cell.delegate = self;

}

PS:I know this DOC is old, but I still add my method to fix the question so when people meet the same problem, they will get help from my answer.

Alfabravo
  • 7,493
  • 6
  • 46
  • 82
吴江伟
  • 39
  • 1
1

I know this post is old but i thought id share because I had a similar experience. I fixed it by setting the userInteractionEnabled to NO for the view of the view controller that was automatically created.

twerdster
  • 4,977
  • 3
  • 40
  • 70
1

use this

    [super.nextResponder touchesBegan:touches withEvent:event];

before this line in your code

    [self.nextResponder touchesBegan:touches withEvent:event];

it means

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
     UITouch* touch = [touches anyObject];
     NSUInteger numTaps = [touch tapCount];
     if (numTaps < 2) {
         [super.nextResponder touchesBegan:touches withEvent:event];
         [self.nextResponder touchesBegan:touches withEvent:event];
     } else {
         [self handleDoubleTap:touch];
     }
}

**Swift 4 Version **

func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
    let touch = touches?.first as? UITouch
    let numTaps: Int? = touch?.tapCount
    if (numTaps ?? 0) < 2 {
        if let aTouches = touches as? Set<UITouch> {
            super.next?.touchesBegan(aTouches, with: event)
            next?.touchesBegan(aTouches, with: event)
        }
    } else {
        handleDoubleTap(touch)
    }
}
Avneesh Agrawal
  • 916
  • 6
  • 11