97

While investigating a memory leak I discovered a problem related to the technique of calling setRootViewController: inside a transition animation block:

[UIView transitionWithView:self.window
                  duration:0.5
                   options:UIViewAnimationOptionTransitionFlipFromLeft
                animations:^{ self.window.rootViewController = newController; }
                completion:nil];

If the old view controller (the one being replaced) is currently presenting another view controller, then the above code does not remove the presented view from the view hierarchy.

That is, this sequence of operations...

  1. X becomes Root View Controller
  2. X presents Y, so that Y's view is on screen
  3. Using transitionWithView: to make Z the new Root View Controller

...looks OK to the user, but the Debug View Hierarchy tool will reveal that Y's view is still there behind Z's view, inside a UITransitionView. That is, after the three steps above, the view hierarchy is:

  • UIWindow
    • UITransitionView
      • UIView (Y's view)
    • UIView (Z's view)

I suspect this is a problem because, at the time of the transition, X's view isn't actually part of the view hierarchy.

If I send dismissViewControllerAnimated:NO to X immediately before transitionWithView:, the resulting view hierarchy is:

  • UIWindow
    • UIView (X's view)
    • UIView (Z's view)

If I send dismissViewControllerAnimated: (YES or NO) to X, then perform the transition in the completion: block, then the view hierarchy is correct. Unfortunately, that interferes with the animation. If animating the dismissal, it wastes time; if not animating, it looks broken.

I'm trying some other approaches (e.g., making a new container view controller class to serve as my root view controller) but haven't found anything that works. I'll update this question as I go.

The ultimate goal is to transition from the presented view to a new root view controller directly, and without leaving stray view hierarchies around.

benzado
  • 82,288
  • 22
  • 110
  • 138
  • I have this same issue currently – Alex Nov 07 '14 at 01:58
  • I just faced the same issue – Jamal Zafar Dec 12 '14 at 10:49
  • Any luck finding a decent solution to this? Same EXACT problem over here. – David Baez May 24 '16 at 02:34
  • @DavidBaez I wound up writing code to aggressively dismiss all view controllers before changing the root. It's very specific to my app, though. Since posting this I've been wondering if swapping the `UIWindow` is the thing to do, but haven't had the time to experiment much. – benzado Jun 01 '16 at 00:15

5 Answers5

120

I had a similar issue recently. I had to manually remove that UITransitionView from the window to fix the problem, then call dismiss on the previous root view controller to ensure its deallocated.

The fix is not really very nice but unless you've found a better way since posting the question, its the only thing I've found to work! viewController is just the newController from your original question.

UIViewController *previousRootViewController = self.window.rootViewController;

self.window.rootViewController = viewController;

// Nasty hack to fix http://stackoverflow.com/questions/26763020/leaking-views-when-changing-rootviewcontroller-inside-transitionwithview
// The presenting view controllers view doesn't get removed from the window as its currently transistioning and presenting a view controller
for (UIView *subview in self.window.subviews) {
    if ([subview isKindOfClass:NSClassFromString(@"UITransitionView")]) {
        [subview removeFromSuperview];
    }
}
// Allow the view controller to be deallocated
[previousRootViewController dismissViewControllerAnimated:NO completion:^{
    // Remove the root view in case its still showing
    [previousRootViewController.view removeFromSuperview];
}];

Swift 3.0

(See edit history for other Swift versions)

For a nicer implementation as a extension on UIWindow allowing an optional transition to be passed in.

extension UIWindow {
    
    /// Fix for http://stackoverflow.com/a/27153956/849645
    func set(rootViewController newRootViewController: UIViewController, withTransition transition: CATransition? = nil) {
        
        let previousViewController = rootViewController
        
        if let transition = transition {
            // Add the transition
            layer.add(transition, forKey: kCATransition)
        }
        
        rootViewController = newRootViewController
        
        // Update status bar appearance using the new view controllers appearance - animate if needed
        if UIView.areAnimationsEnabled {
            UIView.animate(withDuration: CATransaction.animationDuration()) {
                newRootViewController.setNeedsStatusBarAppearanceUpdate()
            }
        } else {
            newRootViewController.setNeedsStatusBarAppearanceUpdate()
        }

        if #available(iOS 13.0, *) {
            // In iOS 13 we don't want to remove the transition view as it'll create a blank screen
        } else {
            // The presenting view controllers view doesn't get removed from the window as its currently transistioning and presenting a view controller
            if let transitionViewClass = NSClassFromString("UITransitionView") {
                for subview in subviews where subview.isKind(of: transitionViewClass) {
                    subview.removeFromSuperview()
                }
            }
        }
        if let previousViewController = previousViewController {
            // Allow the view controller to be deallocated
            previousViewController.dismiss(animated: false) {
                // Remove the root view in case its still showing
                previousViewController.view.removeFromSuperview()
            }
        }
    }
}

Usage:

window.set(rootViewController: viewController)

Or

let transition = CATransition()
transition.type = kCATransitionFade
window.set(rootViewController: viewController, withTransition: transition)
starball
  • 20,030
  • 7
  • 43
  • 238
Rich
  • 8,108
  • 5
  • 46
  • 59
  • 8
    It appears that replacing a root view controller that has presented views (or trying to dealloc a UIWindow which still has presented view controllers) will result in a memory leak. It seems to me that presenting a view controller creates a retain loop with the window, and dismissing the controllers is the only way I've found to break it. I think some internal completion blocks have a strong reference to the window. – Carl Lindberg Feb 03 '15 at 04:12
  • Had an issue with NSClassFromString("UITransitionView") after converting to swift 2.0 – Eugene Braginets Sep 28 '15 at 09:09
  • Still happening in iOS 9 too :( Also I've updated for Swift 2.0 – Rich Oct 14 '15 at 16:19
  • Is this safe to use with App store releases? I mean, isn't `UITransitionView` a private class? Has anyone submitted a version using this solution? Thanks. – user023 Apr 12 '16 at 19:46
  • 1
    @user023 I've used this exact solution in 2 or 3 apps submitted to the App Store without an issue! I guess as you're only checking the type of the class against a string it's fine (it could be any string). What might cause a rejection is having a class named `UITransitionView` in your app as then that's picked up as part of the symbols of the app which I think the App Store uses to check. – Rich Apr 13 '16 at 07:04
  • this solution doesn't work on swift 3 :( Do you know how I should change this for swift 3? – alex_izh Nov 18 '16 at 19:42
  • Plz update the solution: when searching for `UITransitionView` in `subviews` we have to dig deep in cases like with tabbar. – brigadir Mar 21 '17 at 11:49
  • Removing the UITransitionView for in Xcode 11 is causing black screens (removing the wrong subview). I have posted a modified version of the code below with the transition lines to comment out. As far as I can tell so far, the memory leak doesn't appear. – Brett Sep 03 '19 at 22:37
  • @Brett as your solution breaks for older iOS versions, I've put in a proper check – Rich Sep 04 '19 at 09:48
  • @Rich I believe the command might not be necessary at all with Xcode 11 (not based on iOS versions), but I have not done rigorous testing. Thanks for updating your answer though, I'll remove mine now. – Brett Sep 04 '19 at 15:31
  • @Brett it will be based on iOS versions - its just with Xcode 11 you're now building against iOS 13, and obviously running on iOS 13! There is a lot of changes with VC presentation in iOS 13 so the actually issue might either be fixed, or the view hierarchy is different and this breaks this in iOS 13... – Rich Sep 05 '19 at 10:10
  • Still not working properly on iOS 13. If you don't remove the transition screen, it still leaks, even if you remove the transition screen, it also leaks. Not tested yet on iOS 12. Our `rootViewController` is UINavVC -> UIVC -> UIVC -> (Modal) UITabBarVC -> UINavVC -> UIVC – CyberMew Oct 09 '19 at 04:11
5

I faced this issue and it annoyed me for a whole day. I've tried @Rich's obj-c solution and it turns out when I want to present another viewController after that, I will be blocked with a blank UITransitionView.

Finally, I figured out this way and it worked for me.

- (void)setRootViewController:(UIViewController *)rootViewController {
    // dismiss presented view controllers before switch rootViewController to avoid messed up view hierarchy, or even crash
    UIViewController *presentedViewController = [self findPresentedViewControllerStartingFrom:self.window.rootViewController];
    [self dismissPresentedViewController:presentedViewController completionBlock:^{
        [self.window setRootViewController:rootViewController];
    }];
}

- (void)dismissPresentedViewController:(UIViewController *)vc completionBlock:(void(^)())completionBlock {
    // if vc is presented by other view controller, dismiss it.
    if ([vc presentingViewController]) {
        __block UIViewController* nextVC = vc.presentingViewController;
        [vc dismissViewControllerAnimated:NO completion:^ {
            // if the view controller which is presenting vc is also presented by other view controller, dismiss it
            if ([nextVC presentingViewController]) {
                [self dismissPresentedViewController:nextVC completionBlock:completionBlock];
            } else {
                if (completionBlock != nil) {
                    completionBlock();
                }
            }
        }];
    } else {
        if (completionBlock != nil) {
            completionBlock();
        }
    }
}

+ (UIViewController *)findPresentedViewControllerStartingFrom:(UIViewController *)start {
    if ([start isKindOfClass:[UINavigationController class]]) {
        return [self findPresentedViewControllerStartingFrom:[(UINavigationController *)start topViewController]];
    }

    if ([start isKindOfClass:[UITabBarController class]]) {
        return [self findPresentedViewControllerStartingFrom:[(UITabBarController *)start selectedViewController]];
    }

    if (start.presentedViewController == nil || start.presentedViewController.isBeingDismissed) {
        return start;
    }

    return [self findPresentedViewControllerStartingFrom:start.presentedViewController];
}

Alright, now all you have to do is call [self setRootViewController:newViewController]; when you want to switch root view controller.

Longfei Wu
  • 815
  • 10
  • 9
  • Works well, but there is an annoying flash of the presenting view controller right before the root view controller is switched in. Animating the `dismissViewControllerAnimated:` looks perhaps a bit better than no animation. Does avoid the ghost `UITransitionView`s in the view hierarchy though. – pkamb Sep 28 '16 at 02:46
5

I try a simple thing which work for me on iOs 9.3 : just remove the old viewController's view from its hierarchy during dismissViewControllerAnimated completion.

Let's work on X, Y, and Z view as explained by benzado :

That is, this sequence of operations...

  1. X becomes Root View Controller
  2. X presents Y, so that Y's view is on screen
  3. Using transitionWithView: to make Z the new Root View Controller

Which give :

////
//Start point :

let X = UIViewController ()
let Y = UIViewController ()
let Z = UIViewController ()

window.rootViewController = X
X.presentViewController (Y, animated:true, completion: nil)

////
//Transition :

UIView.transitionWithView(window,
                          duration: 0.25,
                          options: UIViewAnimationOptions.TransitionFlipFromRight,
                          animations: { () -> Void in
                                X.dismissViewControllerAnimated(false, completion: {
                                        X.view.removeFromSuperview()
                                    })
                                window.rootViewController = Z
                           },
                           completion: nil)

In my case, X and Y are well dealloc and their's view are no more in hierarchy !

Community
  • 1
  • 1
gbitaudeau
  • 2,207
  • 1
  • 17
  • 13
0

Had a similar issue. In my case I had a viewController hierarchy, and one of the child view controllers had a presented view controller. When I changed then the windows root view controller, for some reason, the presented view controller was still in the memory. So, the solution was to dismiss all view controllers before I change the windows root view controller.

-2

I came to this issue when using this code:

if var tc = self.transitionCoordinator() {

    var animation = tc.animateAlongsideTransitionInView((self.navigationController as VDLNavigationController).filtersVCContainerView, animation: { (context:UIViewControllerTransitionCoordinatorContext!) -> Void in
        var toVC = tc.viewControllerForKey(UITransitionContextToViewControllerKey) as BaseViewController
        (self.navigationController as VDLNavigationController).setFilterBarHiddenWithInteractivity(!toVC.filterable(), animated: true, interactive: true)
    }, completion: { (context:UIViewControllerTransitionCoordinatorContext!) -> Void in

    })
}

Disabling this code, fixed the problem. I managed to get this working by only enabling this transition animation when the filterbar which gets animated is initialised.

It's not really the answer you're looking for, but it could bring you on the right pad for finding your solution.

Antoine
  • 23,526
  • 11
  • 88
  • 94