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.
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:
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.
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:
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.
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:
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:
let backButton = BackBarButtonItem(title: "BACK", menuTitle: "Custom title")
navigationItem.backBarButtonItem = backButton