在 transtionWithView 中更改 rootViewController 时泄漏视图

在调查内存泄漏时,我发现了一个与在转换动画块中调用 setRootViewController:技术相关的问题:

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

如果旧的视图控制器(被替换的视图控制器)当前呈现另一个视图控制器,那么上面的代码不会将呈现的视图从视图层次结构中删除。

也就是说,这个操作序列..。

  1. X 成为根视图控制器
  2. X 表示 Y,因此 Y 的视图显示在屏幕上
  3. 使用 transitionWithView:使 Z 成为新的根视图控制器

... 对用户来说看起来没问题,但是调试视图层次结构工具会显示 Y 的视图仍然在 Z 的视图后面,在 UITransitionView中。也就是说,在上述三个步骤之后,视图层次结构是:

  • UIWindow
    • 视图
      • UIView (Y’s view)
    • UIView (Z 视图)

我怀疑这是一个问题,因为在转换时,X 的视图实际上不是视图层次结构的一部分。

如果我在 transitionWithView:之前将 dismissViewControllerAnimated:NO发送到 X,得到的视图层次结构是:

  • UIWindow
    • UIView (X 的视图)
    • UIView (Z 视图)

如果我将 dismissViewControllerAnimated:(YES 或 NO)发送给 X,然后在 completion:块中执行转换,那么视图层次结构就是正确的。不幸的是,这会影响动画效果。如果动画解雇,它浪费时间; 如果不动画,它看起来破碎。

我正在尝试其他一些方法(例如,创建一个新的容器视图控制器类作为我的根视图控制器) ,但是没有找到任何有效的方法。我会随时更新这个问题。

最终的目标是直接从呈现的视图转换到新的根视图控制器,而不留下任何杂乱的视图层次结构。

21818 次浏览

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)

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.

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.

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 !

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.