web-dev-qa-db-fra.com

Fuites de vues lors du changement de rootViewController dans transitionWithView

En enquêtant sur une fuite de mémoire, j'ai découvert un problème lié à la technique d'appel de 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, le code ci-dessus ne supprime pas la vue présentée de la hiérarchie des 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 est à l'écran
  3. En utilisant transitionWithView: pour faire de Z le nouveau contrôleur de vue racine

... semble correct pour l'utilisateur, mais l'outil Debug View Hierarchy 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 la suivante:

  • 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 réellement partie de la hiérarchie des vues.

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

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

Si j'envoie dismissViewControllerAnimated: (OUI ou NON) à X, puis effectuez la transition dans le completion: block, la hiérarchie des vues est correcte. Malheureusement, cela interfère avec l'animation. Si animer 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, sans laisser de hiérarchies de vues parasites.

90
benzado

J'ai eu un problème similaire récemment. J'ai dû supprimer manuellement ce UITransitionView de la fenêtre pour résoudre le problème, puis appeler ignorer 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é pour fonctionner! viewController n'est que le newController de votre question d'origine.

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 à résoudre votre problème aussi, c'est une douleur absolue dans le cul!

Swift 3.

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

Pour une implémentation plus agréable en tant qu'extension sur UIWindow permettant une transition optionnelle à passer.

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

Usage:

window.set(rootViewController: viewController)

Ou

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

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

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 est à 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 désalloués et leur point de vue n'est plus dans la hiérarchie!

5
gbitaudeau

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

Bon, maintenant tout ce que vous avez à faire est d'appeler [self setRootViewController:newViewController]; lorsque vous souhaitez changer de contrôleur de vue racine.

5
Longfei Wu