web-dev-qa-db-ja.com

transitionWithView内でrootViewControllerを変更するとビューがリークする

メモリリークの調査中に、遷移アニメーションブロック内でsetRootViewController:を呼び出す手法に関連する問題を発見しました。

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

古いView Controller(置き換えられるもの)が現在別のView Controllerを提示している場合、上記のコードは提示されたビューをView階層から削除しません。

つまり、この一連の操作...

  1. Xはルートビューコントローラーになります
  2. XはYを表示するため、Yのビューは画面に表示されます
  3. transitionWithView:を使用してZを新しいルートビューコントローラーにする

...ユーザーには問題ないように見えますが、Debug View Hierarchyツールは、YのビューがUITransitionView内でZのビューの後ろにまだあることを明らかにします。つまり、上記の3つのステップの後、ビュー階層は次のようになります。

  • UIWindow
    • UITransitionView
      • UIView(Yのビュー)
    • UIView(Zのビュー)

移行の時点では、Xのビューは実際にはビュー階層の一部ではないため、これは問題だと思われます。

dismissViewControllerAnimated:NOの直前にtransitionWithView:をXに送信すると、結果のビュー階層は次のようになります。

  • UIWindow
    • UIView(Xのビュー)
    • UIView(Zのビュー)

dismissViewControllerAnimated:(YESまたはNO)をXに送信し、completion:ブロックで遷移を実行すると、ビュー階層が正しくなります。残念ながら、それはアニメーションに干渉します。解雇をアニメーション化すると、時間を無駄にします。アニメートしていない場合、壊れているように見えます。

私は他のいくつかのアプローチ(たとえば、ルートビューコントローラとして機能する新しいコンテナビューコントローラクラスを作成する)を試していますが、機能するものは見つかりませんでした。この質問は、今後更新します。

最終的な目標は、表示されたビューから新しいルートビューコントローラーに、移行するビュー階層を残さずに直接移行することです。

90
benzado

最近、同様の問題が発生しました。この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.

(他の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)
111
Rich

IOs 9.3で動作する簡単なことを試してみます。dismissViewControllerAnimatedの完了時に、古いviewControllerのビューを階層から削除するだけです。

benzado で説明されているように、X、Y、Zビューで作業してみましょう。

つまり、この一連の操作...

  1. Xはルートビューコントローラーになります
  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)

私の場合、XとYは適切にdeallocされており、それらのビューは階層構造ではありません!

5
gbitaudeau

私はこの問題に直面し、それが一日中私を悩ませました。 @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];ルートビューコントローラを切り替えたい場合。

5
Longfei Wu