UI Mage
UI Mage

Reputation: 41

UINavigationControllerDelegate method called twice

I have set up a very simple project with no storyboard, one window and one UINavigationController containing a plain old UIViewController as rootViewController. In the AppDelegate I've set the UINavigationController's delegate to self and implemented

navigationController:didShowViewController:animated which contains 1 line:

NSLog("didShow viewController")

When I launch my app the UINavigationControllerDelegate method navigationController:didShowViewController:animated gets called twice.

AppDelegate:

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDelegate {

    var window: UIWindow?
    var vc1: FirstViewController?
    var nav1: UINavigationController?

    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        NSLog("didShow viewController")
    }

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        vc1 = FirstViewController()
        nav1 = UINavigationController(rootViewController: vc1!)
        nav1?.delegate = self

        window = UIWindow(frame: UIScreen.main.bounds)
        if let window = window {
            window.backgroundColor = UIColor.white
            window.rootViewController = nav1
            window.makeKeyAndVisible()
        }

        return true
    }
}

FirstViewController:

import UIKit

class FirstViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.blue
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

I have tried this in more complicated environments - An app with a UITabBarController and 2 UINavigationControllers as the viewControllers for the UITabBarController. Oddly the UINavigationControllerDelegate methods fire twice the first time a UINavigationController is shown, but will only fire once after that.

Anyone have any insight as to how to correct this? What I believe to be correct behavior based on the documentation is that navigationController:didShowViewController:animated should be only called once in this sample app. I have also checke dot make sure the navigationController and viewController params in the delegate method are the same object.

Thanks in advance!

EDIT: Here's the callstack from each call. All in UIKit code, not mine!

1st call:

29 elements
  - 0 : "0   ???                                 0x0000000115145377 0x0 + 4648620919"
  - 1 : "1   ???                                 0x0000000115145462 0x0 + 4648621154"
  - 2 : "2   Test                                0x0000000105f22d00 main + 0"
  - 3 : "3   Test                                0x0000000105f21e11 _TToFC4Test11AppDelegate20navigationControllerfTCSo22UINavigationController7didShowCSo16UIViewController8animatedSb_T_ + 97"
  - 4 : "4   UIKit                               0x0000000106bab7a8 -[UINavigationController navigationTransitionView:didEndTransition:fromView:toView:] + 1660"
  - 5 : "5   UIKit                               0x0000000106e8839e -[UINavigationTransitionView _notifyDelegateTransitionDidStopWithContext:] + 421"
  - 6 : "6   UIKit                               0x0000000106e88677 -[UINavigationTransitionView _cleanupTransition] + 629"
  - 7 : "7   UIKit                               0x0000000106a58f07 -[UIViewAnimationState sendDelegateAnimationDidStop:finished:] + 222"
  - 8 : "8   UIKit                               0x0000000106a54bcb +[UIViewAnimationState popAnimationState] + 305"
  - 9 : "9   UIKit                               0x0000000106e8810b -[UINavigationTransitionView transition:fromView:toView:] + 2582"
  - 10 : "10  UIKit                               0x0000000106bb01d1 -[UINavigationController _startTransition:fromViewController:toViewController:] + 3301"
  - 11 : "11  UIKit                               0x0000000106bb06b3 -[UINavigationController _startDeferredTransitionIfNeeded:] + 843"
  - 12 : "12  UIKit                               0x0000000106bb17f1 -[UINavigationController __viewWillLayoutSubviews] + 58"
  - 13 : "13  UIKit                               0x0000000106da32bc -[UILayoutContainerView layoutSubviews] + 231"
  - 14 : "14  UIKit                               0x0000000106a9020b -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1268"
  - 15 : "15  QuartzCore                          0x000000010bfbf904 -[CALayer layoutSublayers] + 146"
  - 16 : "16  QuartzCore                          0x000000010bfb3526 _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 370"
  - 17 : "17  QuartzCore                          0x000000010bfb33a0 _ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE + 24"
  - 18 : "18  QuartzCore                          0x000000010bf42e92 _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 294"
  - 19 : "19  QuartzCore                          0x000000010bf6f130 _ZN2CA11Transaction6commitEv + 468"
  - 20 : "20  QuartzCore                          0x000000010bf6fb37 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv + 115"
  - 21 : "21  CoreFoundation                      0x000000010910f717 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23"
  - 22 : "22  CoreFoundation                      0x000000010910f687 __CFRunLoopDoObservers + 391"
  - 23 : "23  CoreFoundation                      0x00000001090f4038 CFRunLoopRunSpecific + 440"
  - 24 : "24  UIKit                               0x00000001069c702f -[UIApplication _run] + 468"
  - 25 : "25  UIKit                               0x00000001069cd0d4 UIApplicationMain + 159"
  - 26 : "26  Test                                0x0000000105f22d37 main + 55"
  - 27 : "27  libdyld.dylib                       0x000000010a19a65d start + 1"
  - 28 : "28  ???                                 0x0000000000000001 0x0 + 1"
  

2nd call:

20 elements
  - 0 : "0   ???                                 0x00000001151456e7 0x0 + 4648621799"
  - 1 : "1   ???                                 0x00000001151457d2 0x0 + 4648622034"
  - 2 : "2   Test                                0x0000000105f22d00 main + 0"
  - 3 : "3   Test                                0x0000000105f21e11 _TToFC4Test11AppDelegate20navigationControllerfTCSo22UINavigationController7didShowCSo16UIViewController8animatedSb_T_ + 97"
  - 4 : "4   UIKit                               0x0000000106ba949b -[UINavigationController viewDidAppear:] + 421"
  - 5 : "5   UIKit                               0x0000000106b7595e -[UIViewController _setViewAppearState:isAnimating:] + 704"
  - 6 : "6   UIKit                               0x0000000106b7863b __64-[UIViewController viewDidMoveToWindow:shouldAppearOrDisappear:]_block_invoke + 42"
  - 7 : "7   UIKit                               0x0000000106b76a7b -[UIViewController _executeAfterAppearanceBlock] + 86"
  - 8 : "8   UIKit                               0x00000001069d992f _runAfterCACommitDeferredBlocks + 634"
  - 9 : "9   UIKit                               0x00000001069c67bc _cleanUpAfterCAFlushAndRunDeferredBlocks + 532"
  - 10 : "10  UIKit                               0x00000001069e957d __84-[UIApplication _handleApplicationActivationWithScene:transitionContext:completion:]_block_invoke_2 + 155"
  - 11 : "11  CoreFoundation                      0x000000010910fb5c __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12"
  - 12 : "12  CoreFoundation                      0x00000001090f4e54 __CFRunLoopDoBlocks + 356"
  - 13 : "13  CoreFoundation                      0x00000001090f45ee __CFRunLoopRun + 894"
  - 14 : "14  CoreFoundation                      0x00000001090f4016 CFRunLoopRunSpecific + 406"
  - 15 : "15  GraphicsServices                    0x000000010b100a24 GSEventRunModal + 62"
  - 16 : "16  UIKit                               0x00000001069cd0d4 UIApplicationMain + 159"
  - 17 : "17  Test                                0x0000000105f22d37 main + 55"
  - 18 : "18  libdyld.dylib                       0x000000010a19a65d start + 1"
  - 19 : "19  ???                                 0x0000000000000001 0x0 + 1"

Upvotes: 4

Views: 1285

Answers (1)

andrew54068
andrew54068

Reputation: 1406

After experimenting, I found that iOS 13 had made this issue for the first UINavigationController in UITabBarController more complicated.

the first UINavigationController won't be called for the call stack [UINavigationController viewDidAppear:]

class ViewController: UIViewController {

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

}

extension ViewController: UINavigationControllerDelegate {

    func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
        print("willShow")
    }

    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        print("didShow")
    }

}

the output will be

willShow
didShow -> this one is from call stack `[UINavigationController __viewWillLayoutSubviews]`

for the first tab.

Which means that if you are using the old workaround down below below iOS 13 then the first time show first VC and only first VC's didShow won't be called.

the new workaround I can think of will be still set the delegate in viewDidLoad but try to stop the first call of VC other than first in TabBarVC.

the didShow will be call in this order

LAUNCH APP

VC1 willShow 
VC1 didShow `[UINavigationController __viewWillLayoutSubviews]`

SELECT SECOND TAB

VC2 willShow 
VC2 didShow `[UINavigationController __viewWillLayoutSubviews]`
VC2 didShow `[UINavigationController viewDidAppear:]` -> this is the one I try to get rid off

SELECT BACK TO FIRST TAB

VC1 willShow 
VC1 didShow `[UINavigationController viewDidAppear:]`

SELECT BACK TO SECOND TAB

VC2 willShow 
VC2 didShow `[UINavigationController viewDidAppear:]`

so make a flag to let other than first UINavigationControllers' didShow not be called only for the first time.

here is the demo project to solve it Demo


below iOS 13 if you only need

func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool)

then there is work around to set like this:

class ViewController: UIViewController {

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        navigationController?.delegate = self
    }

}

extension ViewController: UINavigationControllerDelegate {

    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        // do something
    }

}

but there is a issue that

func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool)

will not be called at the first time cause you set delegate too late, but the second time the same ViewController instance willShow method get called normally.

p.s. if you want to implement didShow method in UINavigationController, you have to set delegate like this:

class NavigationController: UINavigationController {

    override func viewDidAppear(_ animated: Bool) {
        delegate = self
        super.viewDidAppear(animated)
    }

}

extension NavigationController: UINavigationControllerDelegate {

    func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
        print("will")
    }

    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        print("did")
    }

}

since didShow method get called in super.viewDidAppear(animated)

Upvotes: 3

Related Questions