Fuite de vues lors du changement de rootViewController à l'intérieur de transitionWithView

97

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 en cours de remplacement) présente actuellement un autre contrôleur de vue, le code ci-dessus ne supprime pas la vue présentée de la hiérarchie de vues.

Autrement dit, cette séquence d'opérations ...

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

... semble OK pour l'utilisateur, mais l'outil Hiérarchie des vues de débogage révélera que la vue de Y est toujours là derrière la vue de Z, à l'intérieur d'un UITransitionView. Autrement dit, après les trois étapes ci-dessus, la hiérarchie des vues est:

  • UIWindow
    • UITransitionView
      • UIView (vue de Y)
    • UIView (vue de Z)

Je soupçonne que c'est un problème car, au moment de la transition, la vue de X ne fait pas partie de la hiérarchie des vues.

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

  • UIWindow
    • UIView (vue de X)
    • UIView (vue de Z)

Si j'envoie dismissViewControllerAnimated:(OUI ou NON) à X, puis que j'effectue la transition dans le completion:bloc, la hiérarchie de la vue est correcte. Malheureusement, cela interfère avec l'animation. Si vous animez le licenciement, cela fait perdre du temps; sinon animée, elle a l'air cassée.

J'essaie d'autres approches (par exemple, créer une nouvelle classe de contrôleur de vue de conteneur pour servir de contrôleur de vue racine) mais je n'ai rien trouvé qui fonctionne. Je mettrai à jour cette question 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 parasites.

benzado
la source
J'ai ce même problème actuellement
Alex
Je viens de faire face au même problème
Jamal Zafar
Avez-vous de la chance pour trouver une solution décente à cela? Même problème EXACT ici.
David Baez
@DavidBaez J'ai fini par écrire du code pour rejeter de manière agressive tous les contrôleurs de vue avant de changer la racine. C'est très spécifique à mon application, cependant. Depuis la publication de ceci, je me demande si l'échange UIWindowest la chose à faire, mais je n'ai pas eu le temps d'expérimenter beaucoup.
benzado

Réponses:

119

J'ai eu un problème similaire récemment. J'ai dû supprimer manuellement cela UITransitionViewde la fenêtre pour résoudre le problème, puis appeler la fonction de rejet sur le contrôleur de vue racine précédent pour garantir sa désallocation.

Le correctif n'est pas vraiment très sympa mais à moins que vous n'ayez trouvé un meilleur moyen depuis la publication de la question, c'est la seule chose que j'ai trouvée pour fonctionner! viewControllerest juste le newControllerde votre question initiale.

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

J'espère que cela vous aidera aussi à résoudre votre problème, c'est une douleur absolue dans le cul!

Swift 3.0

(Voir l'historique des modifications pour les autres versions de Swift)

Pour une implémentation plus agréable en tant qu'extension UIWindowpermettant de passer une transition facultative.

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

Usage:

window.set(rootViewController: viewController)

Ou

let transition = CATransition()
transition.type = kCATransitionFade
window.set(rootViewController: viewController, withTransition: transition)
Riches
la source
6
Merci. Ça a marché. Partagez si vous trouvez une meilleure approche
Jamal Zafar
8
Il semble que le remplacement d'un contrôleur de vue racine qui a présenté des vues (ou essayer de désallouer une UIWindow qui a toujours présenté des contrôleurs de vue) entraînera une fuite de mémoire. Il me semble que la présentation d'un contrôleur de vue crée une boucle de rétention avec la fenêtre, et rejeter les contrôleurs est le seul moyen que j'ai trouvé pour la casser. Je pense que certains blocs de complétion internes ont une forte référence à la fenêtre.
Carl Lindberg
Problème avec NSClassFromString ("UITransitionView") après la conversion à Swift 2.0
Eugene Braginets
Toujours en cours dans iOS 9: (J'ai également mis à jour pour Swift 2.0
Rich
1
@ user023 J'ai utilisé cette solution exacte dans 2 ou 3 applications soumises à l'App Store sans problème! Je suppose que comme vous ne vérifiez que le type de la classe par rapport à une chaîne, c'est bien (cela peut être n'importe quelle chaîne). Ce qui pourrait provoquer un rejet, c'est d'avoir une classe nommée UITransitionViewdans votre application, car elle est alors sélectionnée dans le cadre des symboles de l'application que je pense que l'App Store utilise pour vérifier.
Riche
5

J'ai fait face à ce problème et cela m'a ennuyé pendant une journée entière. 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 un UITransitionView vide.

Enfin, j'ai compris de cette façon et cela a fonctionné 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];lorsque vous voulez changer de contrôleur de vue racine.

Longfei Wu
la source
Fonctionne bien, mais il y a un flash ennuyeux du contrôleur de vue de présentation juste avant que le contrôleur de vue racine ne soit activé. L'animation des dismissViewControllerAnimated:regards peut-être un peu mieux que pas d'animation. Evite cependant les fantômes UITransitionViewdans la hiérarchie des vues.
pkamb
5

J'essaie une chose simple qui fonctionne pour moi sur iOs 9.3: il suffit de supprimer l'ancienne vue viewController de sa hiérarchie lors de la dismissViewControllerAnimatedfinalisation.

Travaillons sur les vues X, Y et Z comme expliqué par benzado :

Autrement dit, cette séquence d'opérations ...

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

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!

gbitaudeau
la source
0

Eu un problème similaire. Dans mon cas, j'avais une hiérarchie viewController, et l'un des contrôleurs de vue enfants avait un contrôleur de vue présenté. Lorsque j'ai changé le contrôleur de vue racine de Windows, pour une raison quelconque, le contrôleur de vue présenté était toujours dans la mémoire. Donc, la solution était de rejeter tous les contrôleurs de vue avant de changer le contrôleur de vue racine de Windows.

Robert Fogash
la source
-2

Je suis arrivé à ce problème 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

    })
}

La désactivation de ce code a résolu le problème. J'ai réussi à faire fonctionner cela en n'activant cette animation de transition que lorsque la barre de filtre qui s'anime est initialisée.

Ce n'est pas vraiment la réponse que vous recherchez, mais cela pourrait vous amener sur le bon pad pour trouver votre solution.

Antoine
la source