Attempt to present UIViewController on UIViewController whose view is not in the window hierarchy

Viewed 415233

Just started using Xcode 4.5 and I got this error in the console:

Warning: Attempt to present < finishViewController: 0x1e56e0a0 > on < ViewController: 0x1ec3e000> whose view is not in the window hierarchy!

The view is still being presented and everything in the app is working fine. Is this something new in iOS 6?

This is the code I'm using to change between views:

UIStoryboard *storyboard = self.storyboard;
finishViewController *finished = 
[storyboard instantiateViewControllerWithIdentifier:@"finishViewController"];

[self presentViewController:finished animated:NO completion:NULL];
36 Answers

Swift 5 - Background Thread

If an alert controller is executed on a background thread then the "Attempt to present ... whose view is not in the window hierarchy" error may occur.

So this:

present(alert, animated: true, completion: nil)
    

Was fixed with this:

DispatchQueue.main.async { [weak self] in
    self?.present(alert, animated: true, completion: nil)
}

I had similar issue on Swift 4.2 but my view was not presented from the view cycle. I found that I had multiple segue to be presented at same time. So I used dispatchAsyncAfter.

func updateView() {

 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in

// for programmatically presenting view controller 
// present(viewController, animated: true, completion: nil)

//For Story board segue. you will also have to setup prepare segue for this to work. 
 self?.performSegue(withIdentifier: "Identifier", sender: nil)
  }
}

In my situation, I was not able to put mine in a class override. So, here is what I got:

let viewController = self // I had viewController passed in as a function,
                          // but otherwise you can do this


// Present the view controller
let currentViewController = UIApplication.shared.keyWindow?.rootViewController
currentViewController?.dismiss(animated: true, completion: nil)

if viewController.presentedViewController == nil {
    currentViewController?.present(alert, animated: true, completion: nil)
} else {
    viewController.present(alert, animated: true, completion: nil)
}

You can call your segues or present, push codes inside this block:

override func viewDidLoad() {
    super.viewDidLoad()
    OperationQueue.main.addOperation {
        // push or present the page inside this block
    }
}

This kind of warning can mean that You're trying to present new View Controller through Navigation Controller while this Navigation Controller is currently presenting another View Controller. To fix it You have to dismiss currently presented View Controller at first and on completion present the new one. Another cause of the warning can be trying to present View Controller on thread another than main.

I fixed it by moving the start() function inside the dismiss completion block:

self.tabBarController.dismiss(animated: false) {
  self.start()
}

Start contains two calls to self.present() one for a UINavigationController and another one for a UIImagePickerController.

That fixed it for me.

I fixed this error with storing top most viewcontroller into constant which is found within while cycle over rootViewController:

if var topController = UIApplication.shared.keyWindow?.rootViewController {
    while let presentedViewController = topController.presentedViewController {
        topController = presentedViewController
    }
    topController.present(controller, animated: false, completion: nil)
    // topController should now be your topmost view controller
}

With your main window, there will likely always be times with transitions that are incompatible with presenting an alert. In order to allow presenting alerts at any time in your application lifecycle, you should have a separate window to do the job.

/// independant window for alerts
@interface AlertWindow: UIWindow

+ (void)presentAlertWithTitle:(NSString *)title message:(NSString *)message;

@end

@implementation AlertWindow

+ (AlertWindow *)sharedInstance
{
    static AlertWindow *sharedInstance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[AlertWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
    });
    return sharedInstance;
}

+ (void)presentAlertWithTitle:(NSString *)title message:(NSString *)message
{
    // Using a separate window to solve "Warning: Attempt to present <UIAlertController> on <UIViewController> whose view is not in the window hierarchy!"
    UIWindow *shared = AlertWindow.sharedInstance;
    shared.userInteractionEnabled = YES;
    UIViewController *root = shared.rootViewController;
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
    alert.modalInPopover = true;
    [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
        shared.userInteractionEnabled = NO;
        [root dismissViewControllerAnimated:YES completion:nil];
    }]];
    [root presentViewController:alert animated:YES completion:nil];
}

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];

    self.userInteractionEnabled = NO;
    self.windowLevel = CGFLOAT_MAX;
    self.backgroundColor = UIColor.clearColor;
    self.hidden = NO;
    self.rootViewController = UIViewController.new;

    [NSNotificationCenter.defaultCenter addObserver:self
                                           selector:@selector(bringWindowToTop:)
                                               name:UIWindowDidBecomeVisibleNotification
                                             object:nil];

    return self;
}

/// Bring AlertWindow to top when another window is being shown.
- (void)bringWindowToTop:(NSNotification *)notification {
    if (![notification.object isKindOfClass:[AlertWindow class]]) {
        self.hidden = YES;
        self.hidden = NO;
    }
}

@end

Basic usage that, by design, will always succeed:

[AlertWindow presentAlertWithTitle:@"My title" message:@"My message"];

Sadly, the accepted solution did not work for my case. I was trying to navigate to a new View Controller right after unwind from another View Controller.

I found a solution by using a flag to indicate which unwind segue was called.

@IBAction func unwindFromAuthenticationWithSegue(segue: UIStoryboardSegue) {
    self.shouldSegueToMainTabBar = true
}

@IBAction func unwindFromForgetPasswordWithSegue(segue: UIStoryboardSegue) {
    self.shouldSegueToLogin = true
}

Then present the wanted VC with present(_ viewControllerToPresent: UIViewController)

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    if self.shouldSegueToMainTabBar {
        let mainTabBarController = storyboard.instantiateViewController(withIdentifier: "mainTabBarVC") as! MainTabBarController
        self.present(mainTabBarController, animated: true)
        self.shouldSegueToMainTabBar = false
    }
    if self.shouldSegueToLogin {
        let loginController = storyboard.instantiateViewController(withIdentifier: "loginVC") as! LogInViewController
        self.present(loginController, animated: true)
        self.shouldSegueToLogin = false
    }
}

Basically, the above code will let me catch the unwind from login/SignUp VC and navigate to the dashboard, or catch the unwind action from forget password VC and navigate to the login page.

I found this bug arrived after updating Xcode, I believe to Swift 5. The problem was happening when I programatically launched a segue directly after unwinding a view controller.

The solution arrived while fixing a related bug, which is that the user was now able to unwind segues by swiping down the page. This broke the logic of my program.

It was fixed by changing the Presentation mode on all the view controllers from Automatic to Full Screen.

You can do it in the attributes panel in interface builder. Or see this answer for how to do it programatically.

Swift 5

I call present in viewDidLayoutSubviews as presenting in viewDidAppear causes a split second showing of the view controller before the modal is loaded which looks like an ugly glitch

make sure to check for the window existence and execute code just once

var alreadyPresentedVCOnDisplay = false

override func viewDidLayoutSubviews() {
        
    super.viewDidLayoutSubviews()
    
    // we call present in viewDidLayoutSubviews as
    // presenting in viewDidAppear causes a split second showing 
    // of the view controller before the modal is loaded
    
    guard let _ = view?.window else {
        // window must be assigned
        return
    }
    
    if !alreadyPresentedVCOnDisplay {
        alreadyPresentedVCOnDisplay = true
        present(...)
    }
    
}

This works for to present any view controller ,if you have navigation controller available. self.navigationController?.present(MyViewController, animated: true, completion: nil) Also , I can able to present alerts and mail controller also.

The message appear as warning and sometimes the code refuses to work. (!Needs Citation: Newer SDK's might have strict rules).

I have encountered it for more than one reason, mostly complicated viewcontroller scenarios. Here's an example.

Scenario: MainViewController (responsible to load: ViewControllerA & ViewControllerB)

Present ViewControllerA from MainViewController and without dismissing the ViewControllerA you try to present viewControllerB from MainViewController (using a delegate method).

In this scenario, you'd have to make sure your ViewControllerA is dismissed and then the ViewControllerB is called.

Because after presenting ViewControllerA (ViewControllerA becomes responsible for displaying views and viewcontrollers and when MainViewController attempts to load another viewcontoller, it refuses to work with throwing a warning).

This happened to me when I was trying to present onto my navigationController when its view has not been presented onto the view hierarchy yet. The way I solved this is to listen to the NavigationControllerDelegate's didShow method. Once the didShow method is called, I know I can present onto my navigationController.

Note: using dispatchQueueAsync.await(.now()) { //present } does work, but it is hacky and prone to bugs if the view takes long to display onto the view hierarchy.

Using storyboards, I noticed I had a button connected to two @IBActions, these two @IBActions functions present a ViewController each. After I remove one connection, the problem was solve.

Related