在transitionWithView中更改rootViewController时泄漏视图

时间:2014-11-05 17:08:10

标签: ios cocoa-touch uiviewcontroller core-animation

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

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

如果旧视图控制器(正在替换的视图控制器)当前正在呈现另一个视图控制器,则上述代码不会从视图层次结构中删除呈现的视图。

即,这一系列操作......

  1. X成为Root View Controller
  2. X显示Y,因此Y&#39的视图在屏幕上
  3. 使用transitionWithView:使Z成为新的根视图控制器
  4. ...对用户看起来没问题,但是调试视图层次结构工具将在UITransitionView内显示Y视图仍然在Z&#39}的视图后面。也就是说,在上述三个步骤之后,视图层次结构为:

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

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

    如果我在dismissViewControllerAnimated:NO之前立即将transitionWithView:发送给X,则生成的视图层次结构为:

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

    如果我向X发送dismissViewControllerAnimated:(是或否),然后在completion:块中执行转换,则视图层次结构正确。不幸的是,这会干扰动画。如果动画解雇,那就浪费时间;如果没有动画,它看起来很破碎。

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

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

5 个答案:

答案 0 :(得分:109)

我最近遇到过类似的问题。我不得不从窗口中手动删除UITransitionView来解决问题,然后在先前的根视图控制器上调用dismiss来确保它被解除分配。

修复并不是很好,但除非你发布问题以来找到了更好的方法,否则它是我发现的唯一工作方式! viewController只是原始问题中的newController

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

(请参阅编辑其他Swift版本的历史记录)

有一个更好的实现作为UIWindow的扩展,允许传递可选的转换。

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()
        }

        /// 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()
            }
        }
    }
}

用法:

window.set(rootViewController: viewController)

或者

let transition = CATransition()
transition.type = kCATransitionFade
window.set(rootViewController: viewController, withTransition: transition)

答案 1 :(得分:5)

我遇到了这个问题,一整天都让我烦恼。 我已经尝试过@ Rich的obj-c解决方案,当我想要呈现另一个viewController之后,我会被一个空白的UITransitionView阻止。

最后,我想出了这种方式,它对我有用。

- (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];
}

好的,现在您要做的就是在想要切换根视图控制器时调用[self setRootViewController:newViewController];

答案 2 :(得分:4)

我在iOs 9.3上尝试了一个对我有用的简单方法:在dismissViewControllerAnimated完成期间从其层次结构中删除旧的viewController视图。

让我们按照benzado所解释的X,Y和Z视图工作:

  

即,这一系列操作......

     
      
  1. X成为Root View Controller
  2.   
  3. X显示Y,因此Y&#39的视图在屏幕上
  4.   
  5. 使用transitionWithView:使Z成为新的Root View Controller
  6.   

哪个给:

////
//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)

在我的情况下,X和Y是完全dealloc,他们的视图不再是层次结构!

答案 3 :(得分:0)

有类似的问题。在我的情况下,我具有viewController层次结构,并且其中一个子视图控制器具有一个呈现的视图控制器。当我更改Windows根视图控制器时,由于某种原因,所提供的视图控制器仍在内存中。因此,解决方案是在更改Windows根视图控制器之前关闭所有视图控制器。

答案 4 :(得分:-2)

使用此代码时我遇到了这个问题:

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

    })
}

禁用此代码,修复了问题。我设法通过仅在初始化获得动画的过滤器栏时启用此过渡动画来实现此功能。

它并不是您正在寻找的答案,但它可以帮助您找到合适的解决方案。