La fuite des vues lors du changement de rootViewController inside transitionWithView

en enquêtant sur une fuite de mémoire, j'ai découvert un problème lié à la technique d'appel setRootViewController: à l'intérieur d'un bloc d'animation de transition:

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

si l'ancien contrôleur de vue (celui qui est remplacé) présente actuellement un autre contrôleur de vue, alors le code ci-dessus n'enlève pas la vue présentée de la hiérarchie de vue.

C'est-à-dire cette séquence d'opérations...

  1. X devient contrôleur de vue Root
  2. X présente Y, de sorte que la vue de Y est à l'écran
  3. utilisant transitionWithView: pour faire de Z le nouveau contrôleur de vue racine

...cela semble correct pour l'utilisateur, mais L'outil de hiérarchie de la vue de débogage révélera que la vue de Y est toujours derrière la vue de Z, à l'intérieur d'un UITransitionView . C'est-à-dire, après les trois étapes ci-dessus, la hiérarchie de vue est:

  • UIWindow
    • UITransitionView
      • UIView (y's view)
    • UIView (z's view)

je soupçonne que c'est un problème parce que, au moment de la transition, la vue de X ne fait pas réellement partie de la hiérarchie de la vue.

si j'envoie dismissViewControllerAnimated:NO à X immédiatement avant transitionWithView: , la hiérarchie de vue résultante est:

  • UIWindow
    • UIView (X's view)
    • UIView (z's view)

si j'envoie dismissViewControllerAnimated: (oui ou non) à X, alors effectuer la transition dans le bloc completion: , alors la hiérarchie de la vue est correcte. Malheureusement, cela interfère avec l'animation. Si elle anime le renvoi, elle perd du temps; si elle ne l'Anime pas, elle semble cassée.

j'essaie un autre approches (par exemple, faire une nouvelle classe de controller de vue de conteneur pour servir de controller de vue racine) mais n'ont pas trouvé quoi que ce soit qui fonctionne. Je mettrai cette question à jour au fur et à mesure.

le but ultime est de passer directement de la vue présentée à un nouveau contrôleur de vue racine, et sans laisser de hiérarchies de vues errantes.

84
demandé sur benzado 2014-11-05 20:08:10

4 réponses

j'ai eu un problème similaire récemment. J'ai dû retirer manuellement ce UITransitionView de la fenêtre pour corriger le problème, puis appeler dismiss sur le contrôleur de la vue racine précédent pour assurer son désallocation.

la solution n'est pas vraiment très agréable, mais à moins que vous ayez trouvé une meilleure façon depuis l'affichage de la question, c'est la seule chose que j'ai trouvé à travailler! viewController est juste le newController de votre question originale.

UIViewController *previousRootViewController = self.window.rootViewController;

self.window.rootViewController = viewController;

// Nasty hack to fix /q/leaking-views-when-changing-rootviewcontroller-inside-transitionwithview-47123/"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];
}];

j'espère que vous aide à résoudre votre problème aussi, c'est un absolu douleur dans le cul!

Swift 3.0

(voir modifier l'historique pour d'autres versions de Swift)

Pour une meilleure mise en œuvre comme une extension sur UIWindow permettant à une option de transition à passé en.

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

Utilisation:

window.set(rootViewController: viewController)

ou

let transition = CATransition()
transition.type = kCATransitionFade
window.set(rootViewController: viewController, withTransition: transition)
101
répondu Rich 2016-11-18 20:03:54

j'ai fait face à cette question et il m'a ennuyé pendant toute une journée. J'ai essayé la solution obj-C de @Rich et il s'avère que lorsque je veux présenter un autre viewController après cela, je serai bloqué avec une vue uitransitionview vierge.

finalement, j'ai compris de cette façon et ça a marché pour moi.

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

très bien, maintenant tout ce que vous avez à faire est d'appeler [self setRootViewController:newViewController]; quand vous voulez changer le contrôleur de vue racine.

5
répondu Longfei Wu 2016-01-07 03:38:26

j'essaie une chose simple qui fonctionne pour moi sur iOS 9.3 : il suffit de supprimer la vue de viewController de sa hiérarchie pendant l'achèvement dismissViewControllerAnimated .

travaillons sur X, Y, et Z view comme expliqué par benzado :

C'est-à-dire cette séquence d'opérations...

  1. X devient root View Controller
  2. X présente Y, de sorte que la vue de Y est sur écran
  3. utilisant transitionWithView: pour faire Z Le nouveau contrôleur de vue Root

qui donnent:

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

dans mon cas, X et Y sont bien dealloc et leur point de vue n'est plus dans la hiérarchie !

4
répondu gbitaudeau 2017-05-23 10:31:31

je suis venu à cette question en utilisant ce 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

    })
}

désactivant ce code, a résolu le problème. J'ai réussi à obtenir ce fonctionnement en n'activant cette animation de transition que lorsque la barre de filtres qui est animée est initialisée.

ce n'est pas vraiment la réponse que vous recherchez, mais cela pourrait vous amener sur la bonne piste pour trouver votre solution.

-2
répondu Antoine 2015-01-19 13:37:15