Recognize tap on navigation bar title

29,425

Solution 1

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).

Solution 2

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")

}

Solution 3

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.

Solution 4

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")
}

Solution 5

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")

}
Share:
29,425

Related videos on Youtube

Richard
Author by

Richard

Updated on October 09, 2021

Comments

  • Richard
    Richard over 2 years

    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.

  • Richard
    Richard over 9 years
    Perfect solution. I fully agree with not searching through the view hierarchy, like the other examples I found, as might not work in future. Thank you very much.
  • Evgenii
    Evgenii almost 9 years
    This is great solution, actually, works perfect and easy to implement.
  • Ahmed Khalaf
    Ahmed Khalaf over 7 years
    I was almost there by myself, but didn't suspect userInteractionEnabled. +1.
  • Jonny
    Jonny over 7 years
    I have a problem with this solution. On later updates of the text in the uilabel, the label itself jumps to x 0 (far left of screen) for some reason. Seems like a bug in UIKit. Anyway around? (iOS 10.1.1)
  • Jonny
    Jonny over 7 years
    I could not reproduce the above problem 100%, however it seems to be gone for now if I do this AFTER I changed the text of said UILabel: [self.navigationItem.titleView.superview setNeedsLayout];. It indeed stopped the label from jumping to X 0.
  • rob mayoff
    rob mayoff over 7 years
    That would have been my suggestion.
  • Jonny
    Jonny over 7 years
    Don't forget to define self.navigationItem.titleView as in the accepted answer. Default is nil.
  • Federico Malagoni
    Federico Malagoni about 7 years
    Best way to do it.
  • Georg
    Georg about 7 years
    In my case this broke the functionality of the back button after another viewcontroller was pushed onto the stack.
  • Ghulam Rasool
    Ghulam Rasool almost 7 years
    It will break the functionality of of other buttons in navigation bar
  • Ferran Maylinch
    Ferran Maylinch over 6 years
    I just tried my app on iOS11 and the height of 500 doesn't work well: it doesn't get resized so the label may not appear or it may appear in the middle of the screen. I changed it to the nav bar height: 44.
  • Zia
    Zia almost 6 years
    Please see my answer. It takes this approach but keeps the buttons in the navigation bar functional.
  • Karsten
    Karsten almost 6 years
    that's a much easier solution if it's primarily about tapping on the navigation bar and not just on the title in particular! It also reuses an existing UIView and doesn't require the introduction of a new view.
  • bibscy
    bibscy over 5 years
    I didn't know about sender.location(in It's a smart way. I can't vote it enough.
  • WholeCheese
    WholeCheese over 5 years
    Update: my workaround only seems to work in iOS 9. In iOS 12 the hitView is not recognized as my barButton. I do not have an iOS 12 development device so unable to debug this. But I am guessing that in iOS 12 a disabled barButton is not seen as a UIControl or the returned hitView is the underlying navigationBar.
  • Umair_UAS
    Umair_UAS over 4 years
    Million dollars answer (Y)
  • SmileBot
    SmileBot almost 3 years
    Nice solution. I like the viewWillDisappear which removes it for child views. Nice!