在transitionWithView中更改rootViewController时泄漏视图

ben*_*ado 94 cocoa-touch core-animation uiviewcontroller ios

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

[UIView transitionWithView:self.window
                  duration:0.5
                   options:UIViewAnimationOptionTransitionFlipFromLeft
                animations:^{ self.window.rootViewController = newController; }
                completion:nil];
Run Code Online (Sandbox Code Playgroud)

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

也就是说,这一系列操作......

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

...对用户看起来没问题,但Debug View Hierarchy工具将显示Y的视图仍然在Z的视图后面,在a UITransitionView.也就是说,在上面的三个步骤之后,视图层次结构是:

  • 的UIWindow
    • UITransitionView
      • UIView(Y的看法)
    • UIView(Z的观点)

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

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

  • 的UIWindow
    • UIView(X的观点)
    • UIView(Z的观点)

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

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

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

Ric*_*ich 115

我最近遇到过类似的问题.我不得不手动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];
}];
Run Code Online (Sandbox Code Playgroud)

我希望这也有助于你解决你的问题,这是一个绝对痛苦的屁股!

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

        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()
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

window.set(rootViewController: viewController)
Run Code Online (Sandbox Code Playgroud)

要么

let transition = CATransition()
transition.type = kCATransitionFade
window.set(rootViewController: viewController, withTransition: transition)
Run Code Online (Sandbox Code Playgroud)

  • 看来替换已呈现视图的根视图控制器(或尝试解除仍然呈现视图控制器的UIWindow)将导致内存泄漏.在我看来,呈现一个视图控制器会创建一个带窗口的保留循环,并且解除控制器是我发现打破它的唯一方法.我认为一些内部完成块有一个强大的窗口参考. (6认同)
  • 谢谢.有效.如果你找到更好的方法,请分享 (5认同)

Lon*_* Wu 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];
}
Run Code Online (Sandbox Code Playgroud)

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


gbi*_*eau 5

我尝试在iOs 9.3上为我工作的简单方法:只需在dismissViewControllerAnimated完成过程中从其层次结构中删除旧的viewController视图。

让我们按照benzado的说明来研究X,Y和Z视图:

也就是说,此操作顺序...

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

哪个给:

////
//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)
Run Code Online (Sandbox Code Playgroud)

在我的情况下,X和Y很好地解除了分配,并且它们的视图不再处于层次结构中!