Exception: Cannot manually set the delegate on a U

2019-03-06 22:02发布

问题:

My app starts with a login screen that segues to CreateRequestTableViewController, and everything is embedded in a navigation controller, so the back button for the CreateRequest vc goes back to the login screen. I want to ask the user if they're sure they before they're logged out and the navcon pops the vc to show the Login screen again.

I've gotten it to work with the code below, except that after I log back in and move back to the CreateRequest VC (creating a new instance) I get a fatal error:

'NSInternalInconsistencyException', reason: 'Cannot manually set the delegate on a UINavigationBar managed by a controller.'

This puts me in just a little bit over my head. I've tried adding the deinit method that's included in the code below, with no luck.

It's especially strange that it doesn't crash the first time I assign the delegate (or when I set it to nil either), as the text of the error would suggest.

override func viewDidLoad() {
    super.viewDidLoad()
    navigationController?.navigationBar.delegate = self
}

deinit {
    navigationController?.navigationBar.delegate = nil
}

func confirmLogout() {
    let alert = UIAlertController(title: "Log Out", message: "Are you sure you want to log out?",  preferredStyle: .alert)

    let yesButton = UIAlertAction(title: "Log out", style: .destructive) { (_) in
        if let loginVC = self.navigationController?.viewControllers.first as? SignInTableViewController {
            self.navigationController?.popViewController(animated: true)
            loginVC.logOutAll()
        }
    }

    let noButton = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
    alert.addAction(yesButton)
    alert.addAction(noButton)
    present(alert, animated: true, completion: nil)

}

func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {        
    if navigationController?.viewControllers.last is CreateRequestTableViewController {
        confirmLogout()
        return false
    }
    navigationController?.popViewController(animated: true)

    return true
}

回答1:

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
    }
    


回答2:

The error message is clear. You are free to set a navigation controller’s delegate. But you must not set the delegate of its navigation bar; you will break the navigation controller if you do that, because it is the delegate of the bar.