I've solved this problem by deriving custom navigation controller that sets up it's own specialised navigation bar delegate.
This delegate (Forwarder):
- Is set up only once and before the navigation controller takes
control over the navigation bar (in the navigation controller's
initialiser).
 
- Receives UINavigationBarDelegate messages and tries to
call your navigation bar delegate first, then eventually the original navigation
bar delegate (the UINavigationController).
 
The custom navigation controller adds a new "navigationBarDelegate" property that you can use to setup your delegate. You should do that in viewDidAppear:animated: and viewWillDisappear:animated: methods.
Here is the code (Swift 4):
class NavigationController : UINavigationController
{
    fileprivate var originaBarDelegate:UINavigationBarDelegate?
    private var forwarder:Forwarder? = nil
    var navigationBarDelegate:UINavigationBarDelegate?
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        if navigationBar.delegate != nil {
            forwarder = Forwarder(self)
        }
    }
}
fileprivate class Forwarder : NSObject, UINavigationBarDelegate {
    weak var controller:NavigationController?
    init(_ controller: NavigationController) {
        self.controller = controller
        super.init()
        controller.originaBarDelegate = controller.navigationBar.delegate
        controller.navigationBar.delegate = self
    }
    let shouldPopSel = #selector(UINavigationBarDelegate.navigationBar(_:shouldPop:))
    let didPopSel = #selector(UINavigationBarDelegate.navigationBar(_:didPop:))
    let shouldPushSel = #selector(UINavigationBarDelegate.navigationBar(_:shouldPush:))
    let didPushSel = #selector(UINavigationBarDelegate.navigationBar(_:didPush:))
    func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
        if let delegate = controller?.navigationBarDelegate, delegate.responds(to: shouldPopSel) {
            if !delegate.navigationBar!(navigationBar, shouldPop: item) {
                return false
            }
        }
        if let delegate = controller?.originaBarDelegate, delegate.responds(to: shouldPopSel) {
            return delegate.navigationBar!(navigationBar, shouldPop: item)
        }
        return true
    }
    func navigationBar(_ navigationBar: UINavigationBar, didPop item: UINavigationItem) {
        if let delegate = controller?.navigationBarDelegate, delegate.responds(to: didPopSel) {
            delegate.navigationBar!(navigationBar, didPop: item)
        }
        if let delegate = controller?.originaBarDelegate, delegate.responds(to: didPopSel) {
            return delegate.navigationBar!(navigationBar, didPop: item)
        }
    }
    func navigationBar(_ navigationBar: UINavigationBar, shouldPush item: UINavigationItem) -> Bool {
        if let delegate = controller?.navigationBarDelegate, delegate.responds(to: shouldPushSel) {
            if !delegate.navigationBar!(navigationBar, shouldPush: item) {
                return false
            }
        }
        if let delegate = controller?.originaBarDelegate, delegate.responds(to: shouldPushSel) {
            return delegate.navigationBar!(navigationBar, shouldPush: item)
        }
        return true
    }
    func navigationBar(_ navigationBar: UINavigationBar, didPush item: UINavigationItem) {
        if let delegate = controller?.navigationBarDelegate, delegate.responds(to: didPushSel) {
            delegate.navigationBar!(navigationBar, didPush: item)
        }
        if let delegate = controller?.originaBarDelegate, delegate.responds(to: didPushSel) {
            return delegate.navigationBar!(navigationBar, didPush: item)
        }
    }
}
Here is the usage:
- Derive your view controller from UINavigationBarDelegate
 
- Change the class name of the navigation controller in your storyboard from UINavigationController to NavigationController.
 
Put the following code into your view controller
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    (navigationController as? NavigationController)?.navigationBarDelegate = self
}
override func viewWillDisappear(_ animated: Bool) {
    (navigationController as? NavigationController)?.navigationBarDelegate = nil
    super.viewWillDisappear(animated)
}
 
implement one or more of UINavigationBarDelegate methods in your view controller (this is just an example):
func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
    let alert = UIAlertController(title: "Do you really want to leave the page?", message: "All changes will be lost", preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "Stay here", style: .default, handler: nil))
    alert.addAction(UIAlertAction(title: "Leave", style: .destructive, handler: { action in
        self.navigationController?.popViewController(animated: true)
    }))
    self.present(alert, animated: true)
    return false
}