47
votes

Could someone please help me recognise a tap when a user taps on the Title in the Navigation Bar?

I would like to recognise this tap and then animate the tableHeaderView appearing. Possibly sliding the TableView down.

The idea being that the user can then select a quick option (from the tableViewHeader) to re-populate the TableView.

However I cannot recognise any taps.

I'm using Swift.

Thank you.

9

9 Answers

65
votes

UINavigationBar does not expose its internal view hierarchy. There is no supported way to get a reference to the UILabel that displays the title.

You could root around in its view hierarchy “manually” (by searching through its subviews), but that might stop working in a future iOS release because the view hierarchy is private.

One workaround is to create a UILabel and set it as your view controller's navigationItem.titleView. It's up to you to match the style of the default label, which may change in different versions of iOS.

That said, it's pretty easy to set up:

override func didMove(toParentViewController parent: UIViewController?) {
    super.didMove(toParentViewController: parent)

    if parent != nil && self.navigationItem.titleView == nil {
        initNavigationItemTitleView()
    }
}

private func initNavigationItemTitleView() {
    let titleView = UILabel()
    titleView.text = "Hello World"
    titleView.font = UIFont(name: "HelveticaNeue-Medium", size: 17)
    let width = titleView.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)).width
    titleView.frame = CGRect(origin:CGPoint.zero, size:CGSize(width: width, height: 500))
    self.navigationItem.titleView = titleView

    let recognizer = UITapGestureRecognizer(target: self, action: #selector(YourViewController.titleWasTapped))
    titleView.userInteractionEnabled = true
    titleView.addGestureRecognizer(recognizer)
}

@objc private func titleWasTapped() {
    NSLog("Hello, titleWasTapped!")
}

I'm setting the size of the label to its natural width (using sizeThatFits:), but I'm setting its height to 500. The navigation bar will keep the width but shrink the height to the bar's own height. This maximizes the area available for tapping (since the natural height of the label might be only ~22 points but the bar is 44 points high).

35
votes

From the answers, we can tell there are two approaches to do this.

  1. Add a UITapGestureRecognizer to the titleView. This does not seem elegant and requires you to manually set the navigation bar title font so I would not recommend it.
  2. Add a UITapGestureRecognizer to the navigationBar. This seems pretty elegant but the problem with the posted answers that take this approach is that they all result in preventing controls within the navigation bar from working. Here is my implementation of this method that allows your controls to continue working.

// Declare gesture recognizer
var tapGestureRecognizer: UITapGestureRecognizer!

override func viewDidLoad() {

    // Instantiate gesture recognizer
    tapGestureRecognizer = UITapGestureRecognizer(target:self, action: #selector(self.navigationBarTapped(_:)))
}

override func viewWillAppear(_ animated: Bool) {

    // Add gesture recognizer to the navigation bar when the view is about to appear
    self.navigationController?.navigationBar.addGestureRecognizer(tapGestureRecognizer)

    // This allows controlls in the navigation bar to continue receiving touches
    tapGestureRecognizer.cancelsTouchesInView = false
}

override func viewWillDisappear(_ animated: Bool) {

    // Remove gesture recognizer from navigation bar when view is about to disappear
    self.navigationController?.navigationBar.removeGestureRecognizer(tapGestureRecognizer)
}

// Action called when navigation bar is tapped anywhere
@objc func navigationBarTapped(_ sender: UITapGestureRecognizer){

    // Make sure that a button is not tapped.
    let location = sender.location(in: self.navigationController?.navigationBar)
    let hitView = self.navigationController?.navigationBar.hitTest(location, with: nil)

    guard !(hitView is UIControl) else { return }

    // Here, we know that the user wanted to tap the navigation bar and not a control inside it 
    print("Navigation bar tapped")

}
24
votes

This is a solution, albeit not super elegant. In the storyboard just place a regular UIButton over the title and attach it to an IBAction in your ViewController. You may need to do this for each view.

7
votes

A simple approach may be just creating the tap gesture recognizer and attaching it to your navigation bar element.

// on viewDidLoad
let tapGestureRecognizer = UITapGestureRecognizer(target:self, action: #selector(YourViewController.somethingWasTapped(_:)))
self.navigationController?.navigationBar.addGestureRecognizer(tapGestureRecognizer)

func somethingWasTapped(_ sth: AnyObject){
    print("Hey there")
}
7
votes

Bruno's answer was 90% there for me. One thing I noted, though, was that UIBarButtonItem functionality for the Navigation Controller stopped working in other View Controllers, once this gesture recognizer was added to it. To fix this, I just remove the gesture from the Navigation Controller when the view is preparing to disappear:

var tapGestureRecognizer : UITapGestureRecognizer!

override func viewWillAppear(_ animated: Bool) {

  tapGestureRecognizer = UITapGestureRecognizer(target:self, action: #selector(self.navBarTapped(_:)))

  self.navigationController?.navigationBar.addGestureRecognizer(tapGestureRecognizer)

}

override func viewWillDisappear(_ animated: Bool) {

  self.navigationController?.navigationBar.removeGestureRecognizer(tapGestureRecognizer)

}

func navBarTapped(_ theObject: AnyObject){

  print("Hey there")

}
5
votes

Regarding navigationItem.titleView is a UIView indeed, I ended up using a UIButton which gives all the flexibility out-of-box.

override func viewDidLoad() {

    // Create title button
    let titleViewButton = UIButton(type: .system)
    titleViewButton.setTitleColor(UIColor.black, for: .normal)
    titleViewButton.setTitle("Tap Me", for: .normal)

    // Create action listener
    titleViewButton.addTarget(self, action: #selector(YourViewController.titleViewButtonDidTap), for: .touchUpInside)

    // Set the title view with newly created button
    navigationItem.titleView = titleViewButton
}

@objc func titleViewButtonDidTap(_ sender: Any) {
    print("Title did tap")
}
4
votes

There is a simpler and more elegant solution using gesture recognisers (at least for iOS 9 and above).

UITapGestureRecognizer * titleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(titleTapped)];
[self.navigationItem.titleView addGestureRecognizer:titleTapRecognizer];

Then add the title tapped method:

-(void) titleTapped {
    // Called when title is tapped
}
0
votes

Zia: Your guard !(hitView is UIControl) solution has been working well for me while running under iOS 9 but when I run the same code in newer iOS versions it fails to see the hitView as a UIControl when the tapped barButton is disabled.

I have a number of UIBarButtonItems in my navigation bar. When these barButtons are enabled the (hitView is UIControl) in my UITapGestureRecognizer action works correctly and the action function exits. But if the UIBarButtonItem is disabled and the user taps on the button, the (hitView is UIControl) is false and the code proceeds as if the user has tapped the navigation bar.

The only way I have found to work around this problem is to obtain the UIView object for all my barButtons in viewWillAppear with:

button1View = button1Item.value(forKey: "view") as? UIView

etc...

And then in my UITapGestureRecognizer action function I test:

if [button1View, button2View, button3View, button4View].contains(hitView)
{
  return
}

This is an ugly workaround! Any idea why (hitView is UIControl) should return false on a disabled bar button?

0
votes

Just put a Transparent UIButton on custom titleview.

    let maskButton = UIButton(frame: .zero)
    // ....
    // your views...
    // add Transparent button at last
    addSubview(maskButton)
    maskButton.snp.makeConstraints({
        $0.edges.equalToSuperview()
    })
    // listener tap event 
    maskButton.addTarget(self,action:#selector(buttonClicked)....