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...
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:
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:
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.
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];
}];
I hope this helps you fix your problem too, it's an absolute pain in the arse!
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)