iOS 14 Navigation back button menus

Is is possible to opt-out from having the automatic menu show up on a long press in navigation bar "back" button on iOS 14?
Post marked as unsolved Up vote post of manuelC Down vote post of manuelC
2.4k views

12 Replies

Found this on UIBarButtonItem header:
Code Block
/// When non-nil the menu is presented, the gesture used to trigger
/// the menu is based on if the bar button item would normally trigger an action when tapped.
    @available(iOS 14.0, *)
    @NSCopying open var menu: UIMenu?

But setting the backBarButtonItem.menu property to nil does nothing:
Code Block
vc.navigationItem.backBarButtonItem?.menu = nil // still displays the menu

Just want to share my attempt which also had no luck

Code Block
navigationController?.navigationBar.topItem?.backBarButtonItem = UIBarButtonItem(title: "Back", image: nil, primaryAction: action, menu: menu)

This successfully changes the title (navigationItem?.backBarButtonItem does not) but my custom action and menu are ignored- the primaryAction is still to pop and the menu still shows the navigation stack.
I found that setting a custom back button will disable the menu:

 
Code Block
let backButton = UIBarButtonItem(title: "< back",
                                 style: .done,
                                 target: self,
                                 action: #selector(popVC(sender:)))
vc.navigationItem.leftBarButtonItem = backButton
@objc
func popVC(sender: UIBarButtonItem) {
  navigationController?.popViewController(animated: true)
}


The back button menu cannot be disabled, as long as the navigation bar shows its native back button, the menu is available.

You can control what is in the menu in the same way you might control navigation in your navigation stack in the first place – if there is no back button displayed when a particular navigation item is top-most, then you will not be able to navigate past that point with the menu.

Is there a specific issue you are facing that leads you to want to disable the menu?
I have a bit deferent question.
How can I change a texts on items from the automatic menu displayed. Currently it Displays only names of the back buttons for each VC in stack.
What I need is to display a titles of the View controllers there.
The navigation menu titles come from the navigation item's back button title first on the assumption that clients who choose to override that title do so because it is a better title than the navigation item's title. By default the view controller title is synced with the navigation item title.
@Rincewind you said: "The navigation menu titles come from the navigation item's back button title first on the assumption that clients who choose to override that title do so because it is a better title than the navigation item's title."

Is it possible to exclude the navigation menu items that have an empty/null title? Most apps I'm familar with disable the back button text by setting the item to nil/"" and thusly we're getting a list of empty items in the popover.

Either that or allow us to disable it?
Same as @McCarron here:
The back navigation title feature is introduced exactly to give an opportunity to specify a special case of the Back title where the title of the View won't be feasible/optimal. Why then limiting the navigation menu to only back titles? There are a lot of use cases where the Back navigation title works the best in the context of the screen, but doesn't work at all outside of it in some sort of the vertical stack list with no context.

That would be much better to either give an opportunity to override it the same way we a given with the .backBarButtonItem
Enter
Using subclass of BarButtonItem which overrides menu property and return nil can disable the menu.
But I'm not confident whether this is correct way or not.

Disabling the menu for the UIBarButtonItem back button


It can be done by subclassing UIBarButtonItem. Setting the menu to nil on a UIBarButtonItem doesn't work, but you can override the menu property and prevent setting it in the first place.

Code Block
class BackBarButtonItem: UIBarButtonItem {
  @available(iOS 14.0, *)
  override var menu: UIMenu? {
    set {
      /* Don't set the menu here */
      /* super.menu = menu */
    }
    get {
      return super.menu
    }
  }
}


Then you can configure the back button in your view controller the way you like, but using BackBarButtonItem instead of UIBarButtonItem:

Code Block
let backButton = BackBarButtonItem(title: "BACK", style: .plain, target: nil, action: nil)
navigationItem.backBarButtonItem = backButton


This is the preferred way because you set the backBarButtonItem only once in your view controller's navigation item, and then whatever view controller it will be pushing, the pushed controller will show the back button automatically on the nav bar. If using leftBarButtonItem instead of backBarButtonItem, you will have to set it on every view controller that will be pushed.

Another thing with backBarButtonItem is that you keep the cool animation on the nav bar during transition.


Custom menu item title


It's possible to set a custom title on the menu item for the back button instead of disabling the menu entirely. Now that we intercept the menu setter in BackBarButtonItem, the menu items can be replaced. We only have to provide a new action for returning to the view controller, which we lose when we set our own menu item.

Code Block
class BackBarButtonItem: UIBarButtonItem {
/* Contains a custom title for the menu item + a handler to return to the view controller. */
  var menuAction: UIAction!
   
  convenience init(title: String, menuTitle: String, menuHandler: @escaping UIActionHandler) {
    self.init(title: title, style: .plain, target: nil, action: nil)
    menuAction = UIAction(title: menuTitle, handler: menuHandler)
  }
   
  @available(iOS 14.0, *)
  override var menu: UIMenu? {
    set {
      super.menu = newValue?.replacingChildren([menuAction])
    }
    get {
      return super.menu
    }
  }
}


Then in your view controller, pass a custom menu title to the back button and provide a handler to get back to the view controller:

Code Block
let backButton = BackBarButtonItem(title: "BACK", menuTitle: "Custom title", menuHandler: { _ in
  self.navigationController?.popToViewController(self, animated: true)
})
navigationItem.backBarButtonItem = backButton


Notice that doing so means that every backBarButtonItem will show a menu with a single item, the custom title you set to it. This is only useful when pushing one other view controller onto the navigation stack (not pushing again until you get back to the root controller).


Menu containing the entire stack with custom titles


If you want the menu to be showing custom titles for the entire stack on every backBarMenuItem, there's a bit more work to be done.
It's not just a matter of replacing the last item in the menu, but reconstructing the menu from scratch for each backBarMenuItem for the entire stack behind it, because that's what the system does so we have to do the same.

Each BackBarButtonItem will have a custom title, but the menu will be created by a UINavigationController subclass for the entire stack at each step in the navigation. This means that BackBarButtonItem should allow setting our custom menu, and to do that it simply checks if the custom title is in the menu.

Code Block
class BackBarButtonItem: UIBarButtonItem {
  var menuTitle: String?
   
  convenience init(title: String, menuTitle: String) {
    self.init(title: title, style: .plain, target: nil, action: nil)
    self.menuTitle = menuTitle
  }
   
  @available(iOS 14.0, *)
  override var menu: UIMenu? {
    set {
      if newValue?.children.last?.title == menuTitle {
        super.menu = newValue
      }
    }
    get {
      return super.menu
    }
  }
}


Create a new menu for the entire stack in NavigationController and set it on the current backBarButtonItem:

Code Block
class NavigationController: UINavigationController, UINavigationControllerDelegate {
  init() {
    super.init(rootViewController: ViewController())
    delegate = self
  }
   
  func navigationController(_ navigationController: UINavigationController,
                 didShow viewController: UIViewController, animated: Bool) {
    if #available(iOS 14.0, *) {
      updateBackButtonMenu()
    }
  }
   
  @available(iOS 14.0, *)
  private func updateBackButtonMenu() {
    var menuItems = [UIMenuElement]()
    for navigationItem in navigationBar.items ?? [] {
      guard let backButton = navigationItem.backBarButtonItem as? BackBarButtonItem else { continue }
      guard let menuTitle = backButton.menuTitle else { continue }
      let action = UIAction(title: menuTitle) { _ in
        if let viewController = self.viewControllers.first(where: { $0.navigationItem == navigationItem }) {
          self.popToViewController(viewController, animated: true)
        }
      }
      menuItems.append(action)
    }
    navigationBar.topItem?.backBarButtonItem?.menu = UIMenu(items: menuItems)
  }
}
extension UIMenu {
  convenience init(items: [UIMenuElement]) {
    self.init(title: "", image: nil, identifier: nil, options: [], children: items)
  }
}


Then configuring the backBarButtonItem on your view controller is as simple as:

Code Block
let backButton = BackBarButtonItem(title: "BACK", menuTitle: "Custom title")
navigationItem.backBarButtonItem = backButton


I haven't found a solution for disabling the menu other than the solution that @andreimarincas shared, but if anyone else is coming to this post for a way to hide the back button title while still showing titles in the menu, there is an easier way than what that post mentions, using UINavigationItem.BackButtonDisplayMode. Setting this property (new in iOS 14) to .minimal will only show the back button indicator instead of the title. I've been able to make this work nicely by putting the following in viewDidLoad:

Code Block swift
if #available(iOS 14.0, *) {
    self.navigationItem.backButtonTitle = "Custom menu title"
    self.navigationItem.backButtonDisplayMode = .minimal
} else {
    self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}

The value you set for backButtonTitle will only show in the navigation menu, and you can still hide the title if iOS 14 is not available.