[ios] How do I get the RootViewController from a pushed controller?

So, I push a view controller from RootViewController like:

[self.navigationController pushViewController:anotherViewController animated:YES] ;

BUT, FROM anotherViewController now, I want to access the RootViewController again.

I'm trying

// (inside anotherViewController now)
///RootViewController *root = (RootViewController*)self.parentViewController ; // No.
// err
RootViewController *root = (RootViewController*)[self.navigationController.viewControllers objectAtIndex:0] ; // YES!! it works

I'm not sure WHY this works and I'm not sure if its the best way to do it. Can somebody comment on a better way to get the RootViewController from a controller you've pushed into that RootViewController's navigationController and whether or not the way I've done it is reliable or not?

This question is related to ios iphone uinavigationcontroller

The answer is


How about asking the UIApplication singleton for its keyWindow, and from that UIWindow ask for the root view controller (its rootViewController property):

UIViewController root = [[[UIApplication sharedApplication] keyWindow] rootViewController];

This worked for me:

When my root view controller is embedded in a navigation controller:

UINavigationController * navigationController = (UINavigationController *)[[[[UIApplication sharedApplication] windows] firstObject] rootViewController];
RootViewController * rootVC = (RootViewController *)[[navigationController viewControllers] firstObject];

Remember that keyWindow is deprecated.


Here I came up with universal method to navigate from any place to root.

  1. You create a new Class file with this class, so that it's accessible from anywhere in your project:

    import UIKit
    
    class SharedControllers
    {
        static func navigateToRoot(viewController: UIViewController)
        {
            var nc = viewController.navigationController
    
            // If this is a normal view with NavigationController, then we just pop to root.
            if nc != nil
            {
                nc?.popToRootViewControllerAnimated(true)
                return
            }
    
            // Most likely we are in Modal view, so we will need to search for a view with NavigationController.
            let vc = viewController.presentingViewController
    
            if nc == nil
            {
                nc = viewController.presentingViewController?.navigationController
            }
    
            if nc == nil
            {
                nc = viewController.parentViewController?.navigationController
            }
    
            if vc is UINavigationController && nc == nil
            {
                nc = vc as? UINavigationController
            }
    
            if nc != nil
            {
                viewController.dismissViewControllerAnimated(false, completion:
                    {
                        nc?.popToRootViewControllerAnimated(true)
                })
            }
        }
    }
    
  2. Usage from anywhere in your project:

    {
        ...
        SharedControllers.navigateToRoot(self)
        ...
    }
    

A slightly less ugly version of the same thing mentioned in pretty much all these answers:

UIViewController *rootViewController = [[self.navigationController viewControllers] firstObject];

in your case, I'd probably do something like:

inside your UINavigationController subclass:

- (UIViewController *)rootViewController
{
    return [[self viewControllers] firstObject];
}

then you can use:

UIViewController *rootViewController = [self.navigationController rootViewController];

edit

OP asked for a property in the comments.

if you like, you can access this via something like self.navigationController.rootViewController by just adding a readonly property to your header:

@property (nonatomic, readonly, weak) UIViewController *rootViewController;

I encounter a strange condition.

self.viewControllers.first is not root viewController always.

Generally, self.viewControllers.first is root viewController indeed. But sometimes it's not.

class MyCustomMainNavigationController: UINavigationController {

    function configureForView(_ v: UIViewController, animated: Bool) {
        let root = self.viewControllers.first
        let isRoot = (v == root)
    
        // Update UI based on isRoot
        // ....
    }
}

extension MyCustomMainNavigationController: UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController, 
        willShow viewController: UIViewController, 
        animated: Bool) {

        self.configureForView(viewController, animated: animated)
    }
}

My issue:

Generally, self.viewControllers.first is root viewController. But, when I call popToRootViewController(animated:), and then it triggers navigationController(_:willShow:animated:). At this moment, self.viewControllers.first is NOT root viewController, it's the last viewController which will disappear.

Summary

  • self.viewControllers.first is not always root viewController. Sometime, it will be the last viewController.

So, I suggest to keep rootViewController by property when self.viewControllers have ONLY one viewController. I get root viewController in viewDidLoad() of custom UINavigationController.

class MyCustomMainNavigationController: UINavigationController {
    fileprivate var myRoot: UIViewController!
    override func viewDidLoad() {

        super.viewDidLoad()

        // My UINavigationController is defined in storyboard. 
        // So at this moment, 
        // I can get root viewController by `self.topViewController!`
        let v = self.topViewController!
        self.myRoot = v
    }

}

Enviroments:

  • iPhone 7 with iOS 14.0.1
  • Xcode 12.0.1 (12A7300)

For all who are interested in a swift extension, this is what I'm using now:

extension UINavigationController {
    var rootViewController : UIViewController? {
        return self.viewControllers.first
    }
}

Swift version :

var rootViewController = self.navigationController?.viewControllers.first

ObjectiveC version :

UIViewController *rootViewController = [self.navigationController.viewControllers firstObject];

Where self is an instance of a UIViewController embedded in a UINavigationController.


As an addition to @dulgan's answer, it is always a good approach to use firstObject over objectAtIndex:0, because while first one returns nil if there is no object in the array, latter one throws exception.

UIViewController *rootViewController = self.navigationController.rootViewController;

Alternatively, it'd be a big plus for you to create a category named UINavigationController+Additions and define your method in that.

@interface UINavigationController (Additions)

- (UIViewController *)rootViewController;

@end

@implementation UINavigationController (Additions)

- (UIViewController *)rootViewController
{
    return self.viewControllers.firstObject;
}

@end

Examples related to ios

Adding a UISegmentedControl to UITableView Crop image to specified size and picture location Undefined Symbols error when integrating Apptentive iOS SDK via Cocoapods Keep placeholder text in UITextField on input in IOS Accessing AppDelegate from framework? Autoresize View When SubViews are Added Warp \ bend effect on a UIView? Speech input for visually impaired users without the need to tap the screen make UITableViewCell selectable only while editing Xcode 12, building for iOS Simulator, but linking in object file built for iOS, for architecture arm64

Examples related to iphone

Detect if the device is iPhone X Xcode 8 shows error that provisioning profile doesn't include signing certificate Access files in /var/mobile/Containers/Data/Application without jailbreaking iPhone Certificate has either expired or has been revoked Missing Compliance in Status when I add built for internal testing in Test Flight.How to solve? cordova run with ios error .. Error code 65 for command: xcodebuild with args: "Could not find Developer Disk Image" Reason: no suitable image found iPad Multitasking support requires these orientations How to insert new cell into UITableView in Swift

Examples related to uinavigationcontroller

How to enable back/left swipe gesture in UINavigationController after setting leftBarButtonItem? How to hide a navigation bar from first ViewController in Swift? Execute action when back bar button of UINavigationController is pressed Programmatically navigate to another view controller/scene How to force view controller orientation in iOS 8? presenting ViewController with NavigationViewController swift How to Navigate from one View Controller to another using Swift UINavigationBar Hide back Button Text How to check if a view controller is presented modally or pushed on a navigation stack? Trying to handle "back" navigation button action in iOS