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


97

メモリリークを調査しているとき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を新しいルートビューコントローラにする

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

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

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

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

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

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

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

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


私は現在、同じ問題を抱えています
Alex

私はちょうど同じ問題に直面しました
Jamal Zafar

これに対する適切な解決策を見つける幸運はありますか?ここで同じ正確な問題。
David Baez

@DavidBaezルートを変更する前に、すべてのビューコントローラーを積極的に閉じるコードを作成しました。ただし、これは私のアプリに固有のものです。これを投稿して以来、スワップUIWindowすることで対処できるかどうか疑問に思っていましたが、あまり実験する時間はありませんでした。
ベンザド2016年

回答:


119

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

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

使用法:

window.set(rootViewController: viewController)

または

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

6
ありがとう。出来た。より良いアプローチを見つけたら共有してください
Jamal Zafar

8
ビューを表示したルートビューコントローラーを置き換える(またはまだビューコントローラーを表示しているUIWindowの割り当てを解除しようとする)と、メモリリークが発生するようです。私には、ビューコントローラーを表示するとウィンドウで保持ループが作成され、コントローラーを閉じることがそれを壊すための唯一の方法であるように見えます。一部の内部完了ブロックはウィンドウに強い参照があると思います。
カールリンドバーグ、

swift 2.0への変換後にNSClassFromString( "UITransitionView")に問題があった
Eugene Braginets '28 / 09/15

iOS 9でも引き続き発生します:(また、Swift 2.0用に更新しました
Rich

1
@ user023 App Storeに送信された2つまたは3つのアプリでこのソリューションを問題なく使用しました!あなたはクラスのタイプを文字列に対してチェックしているだけなので、それは問題ありません(それは任意の文字列である可能性があります)。拒否の原因となる可能性があるのはUITransitionView、アプリに名前が付けられたクラスがあることです。このクラスは、App Storeがチェックに使用するアプリのシンボルの一部としてピックアップされます。
リッチ

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];。ルートビューコントローラを切り替えたいときに呼び出すだけです。


うまく機能しますが、ルートビューコントローラーが切り替わる直前に、表示しているビューコントローラーのいらいらするフラッシュがありdismissViewControllerAnimated:ます。UITransitionViewただし、ビュー階層のゴーストは回避されます。
pkamb 2016

5

私はiO 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は十分に割り当て解除されており、それらのビューは階層にありません。


0

同様の問題がありました。私の場合、viewController階層があり、子のビューコントローラーの1つにビューコントローラーがありました。Windowsルートビューコントローラーを変更したとき、何らかの理由で、表示されたビューコントローラーがまだメモリ内にありました。したがって、解決策は、ウィンドウのルートビューコントローラーを変更する前にすべてのビューコントローラーを閉じることでした。


-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

    })
}

このコードを無効にして、問題を修正しました。アニメーション化されたフィルターバーが初期化されたときにのみ、この遷移アニメーションを有効にすることでこれを機能させることができました。

それは本当にあなたが探している答えではありませんが、あなたの解決策を見つけるための正しいパッドにあなたを連れて行くかもしれません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.