16

The question in the first place was:

When you have a tableView how to implement that the user can tap the NavigationBar to scroll all the way to the top.

Solution:

- (void)viewDidLoad {
    UITapGestureRecognizer* tapRecon = [[UITapGestureRecognizer alloc]
              initWithTarget:self action:@selector(navigationBarDoubleTap:)];
    tapRecon.numberOfTapsRequired = 2;
    [navController.navigationBar addGestureRecognizer:tapRecon];
    [tapRecon release];
}

- (void)navigationBarDoubleTap:(UIGestureRecognizer*)recognizer {
    [tableView setContentOffset:CGPointMake(0,0) animated:YES];
}

Which works like a charm!

But Drarok pointed out an issue:

This approach is only viable if you don't have a back button, or rightBarButtonItem. Their click events are overridden by the gesture recognizer

My question:

How can I have the nice feature that my NavigationBar is clickable but still be able to use the back buttons in my app?

So either find a different solution that doesn't override the back button or find a solution to get the back button back to working again :)

Octoshape
  • 1,131
  • 8
  • 26

6 Answers6

15

Rather than using location view, I solved this by checking the class of the UITouch.

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
    return (![[[touch view] class] isSubclassOfClass:[UIControl class]]);
}

Note that the nav buttons are of type UINavigationButton which is not exposed, hence the subclass checking.

This method goes in the class you designate as the delegate of the gesture recognizer. If you're just getting started with gesture recognizers, note that the delegate is set separately from the target.

Ben Flynn
  • 18,524
  • 20
  • 97
  • 142
7

UIGestureRecognizerDelegate has a method called "gestureRecognizer:shouldReceiveTouch". If you are able to point out if the touch's view is the button, just make it return "NO", otherwise return "YES" and you're good to go.

Stavash
  • 14,244
  • 5
  • 52
  • 80
  • Hmm I found the method and everything and got it to fire. But the UITouch that's handed to the method is nil :-/ I've never worked with UITouch or UIGestureRecognizers. How do I get the position of the touch that fired the delegate method? – Octoshape Oct 14 '11 at 23:02
  • You could try "locationInView", passing it the navigation bar's view. If everything is set up correctly (make sure delegate is assigned), you should receive a relative CGPoint of the touch in the given view. – Stavash Oct 14 '11 at 23:06
  • Funny.. that's exactly what I tried first :) but got a EXC_BAD_ACCESS, upon checking the UITouch given to the delegate method I saw that it was nil :-/ I don't really understand why because I think without an UITouch the method doesn't get called? – Octoshape Oct 14 '11 at 23:08
  • Nevermind I made some mistake somewhere.. let me try again .. sorry – Octoshape Oct 14 '11 at 23:09
  • Amazing! Works like a charm :)) – Octoshape Oct 14 '11 at 23:14
  • You seem to be quite smart if I may say so.. Would you mind giving [this](http://stackoverflow.com/questions/7774796/uiscrollview-backgroundcolor-with-pattern-creates-a-line) question a try? :) Don't have to if you don't want to.. – Octoshape Oct 15 '11 at 00:08
  • Took a look, sorry I don't have any experience with pattern images as background. – Stavash Oct 15 '11 at 10:56
  • 1
    Hi, Using "locationInView" returns touch points in x & y coordinates. But my back bar button item's width keep changing. Because, I set the back bar button's title value with respect to the clicked cells in my table view controller. So, how can I identify whether the back bar button item is clicked? Even I tried to get the class name by `[touch view].class`. But it always return "UINavigaionBar" class for both titleView click & back bar button click event – Confused Oct 20 '11 at 13:26
5

UIGestureRecognizer also has an attribute @property(nonatomic) BOOL cancelsTouchesInView. From the documentation: A Boolean value affecting whether touches are delivered to a view when a gesture is recognized.

So if you simply do

tapRecon.cancelsTouchesInView = NO;

this might be an even simpler solution, depending on your use case. This is how I do it in my app.

When pressing a button in the navigation bar though, its action is executed (as desired), but the UIGestureRecognizer's action is executed as well. If that doesn't bother you, then that would be the simplest solution I could think of.

Dennis
  • 2,223
  • 3
  • 27
  • 36
2

iOS7 version:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
    CGPoint point = [touch locationInView:touch.view];
    UINavigationBar *naviagationBar = (UINavigationBar *)touch.view;
    NSString *navigationItemViewClass = [NSString stringWithFormat:@"UINavigationItem%@%@",@"Button",@"View"];
    for (id subview in naviagationBar.subviews) {

        if (([subview isKindOfClass:[UIControl class]] ||
             [subview isKindOfClass:NSClassFromString(navigationItemViewClass)]) &&
             [subview pointInside:point withEvent:nil]) {

            return NO;
        }
    }
    return YES;
}

EDIT:

Someting on the corner of back button gesture is still overwritten, so you this code insted of pointInside:withEvent:

CGRectContainsPoint((CGRect){ .origin = subview.frame.origin, .size = CGSizeMake(subview.frame.size.width + 16, subview.frame.size.height)}, point)
Michal Zaborowski
  • 5,039
  • 36
  • 35
0

Xamarin.iOS does not expose C# wrappers for Objective-C classes in the private API, so the neat subclass checking suggested by @ben-flynn above won't work here.

A somewhat hackish workaround is to check the Description field of the views:

navigationTitleTap = new UITapGestureRecognizer (tap => DidTapNavigationTitle());

navigationTitleTap.ShouldReceiveTouch = (recognizer, touch) => 
    touch.View.Subviews.Any(sv => 
        // Is this the NavigationBar's title or prompt?
        (sv.Description.StartsWith("<UINavigationItemView") || sv.Description.StartsWith("<UINavBarPrompt")) &&
        // Was the nested label actually tapped?
        sv.Subviews.OfType<UILabel>().Any(label =>
            label.Frame.Contains(touch.LocationInView(sv))));

NavigationController.NavigationBar.AddGestureRecognizer (navigationTitleTap);

The Linq collection filter .OfType<T> is convenient though when fishing for certain types in the view hierarchy.

crishoj
  • 5,660
  • 4
  • 32
  • 31
0

This worked for me, It is based on Stavash answer. I use the view property of the gesture recognizer to return YES/NO in the delegate method.

It is an old app so obviously this is not ARC, does not use new layout stuff nor NSAttributed strings. I leave that to you :p

- (void)viewDidLoad
{
    ...
    CGRect r = self.navigationController.navigationBar.bounds;
    UILabel *titleView = [[UILabel alloc] initWithFrame:r];
    titleView.autoresizingMask = 
      UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
    titleView.textAlignment = NSTextAlignmentCenter;
    titleView.font = [UIFont systemFontOfSize:[UIFont systemFontSize]];
    titleView.text = self.title;
    titleView.userInteractionEnabled = YES;
    UITapGestureRecognizer *tgr =
      [[UITapGestureRecognizer alloc] initWithTarget:self
                                              action:@selector(titleViewWasTapped:)];
    tgr.numberOfTapsRequired = 1;
    tgr.numberOfTouchesRequired = 1;
    tgr.delegate = self;
    [titleView addGestureRecognizer:tgr];
    [tgr release];
    self.navigationItem.titleView = titleView;
    [titleView release];
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
       shouldReceiveTouch:(UITouch *)touch
{
    // This method is needed because the navigation bar with back
    // buttons will swallow touch events
    return (gestureRecognizer.view == self.navigationItem.titleView);
}

Then you use the gesture recogniser as usual

- (void)titleViewWasTapped:(UIGestureRecognizer *)gestureRecognizer
{
    if (gestureRecognizer.state != UIGestureRecognizerStateRecognized) {
        return;
    }
    ...
}
nacho4d
  • 43,720
  • 45
  • 157
  • 240