Fugas de vistas al cambiar rootViewController dentro de transitionWithView

97

Mientras investigaba una fuga de memoria, descubrí un problema relacionado con la técnica de llamar setRootViewController:dentro de un bloque de animación de transición:

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

Si el controlador de vista anterior (el que se está reemplazando) presenta actualmente otro controlador de vista, entonces el código anterior no elimina la vista presentada de la jerarquía de vistas.

Es decir, esta secuencia de operaciones ...

  1. X se convierte en controlador de vista raíz
  2. X presenta Y, por lo que la vista de Y está en pantalla
  3. Usando transitionWithView:para hacer de Z el nuevo controlador de vista raíz

... se ve bien para el usuario, pero la herramienta Jerarquía de vista de depuración revelará que la vista de Y todavía está detrás de la vista de Z, dentro de a UITransitionView. Es decir, después de los tres pasos anteriores, la jerarquía de vistas es:

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

Sospecho que esto es un problema porque, en el momento de la transición, la vista de X no es realmente parte de la jerarquía de vista.

Si envío dismissViewControllerAnimated:NOa X inmediatamente antes transitionWithView:, la jerarquía de vista resultante es:

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

Si envío dismissViewControllerAnimated:(SÍ o NO) a X, luego realizo la transición en el completion:bloque, entonces la jerarquía de vista es correcta. Desafortunadamente, eso interfiere con la animación. Si anima el despido, pierde tiempo; si no está animando, parece roto.

Estoy intentando algunos otros enfoques (por ejemplo, crear una nueva clase de controlador de vista de contenedor para que sirva como mi controlador de vista raíz) pero no he encontrado nada que funcione. Actualizaré esta pregunta sobre la marcha.

El objetivo final es hacer la transición de la vista presentada a un nuevo controlador de vista raíz directamente y sin dejar jerarquías de vista perdidas.

benzado
fuente
Tengo el mismo problema actualmente
Alex
Acabo de enfrentar el mismo problema
Jamal Zafar
¿Alguna suerte para encontrar una solución decente a esto? El mismo problema EXACTO aquí.
David Baez
@DavidBaez Terminé escribiendo código para descartar agresivamente todos los controladores de vista antes de cambiar la raíz. Sin embargo, es muy específico para mi aplicación. Desde que publiqué esto, me he estado preguntando si lo mejor UIWindowes cambiarlo, pero no he tenido tiempo de experimentar mucho.
benzado

Respuestas:

119

Recientemente tuve un problema similar. Tuve que eliminarlo manualmente UITransitionViewde la ventana para solucionar el problema, luego llamar a descartar en el controlador de vista raíz anterior para asegurarme de que se desasigne.

La solución no es muy buena, pero a menos que haya encontrado una mejor manera desde que publiqué la pregunta, ¡es lo único que he encontrado que funciona! viewControlleres solo el newControllerde tu pregunta original.

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

Espero que esto también te ayude a solucionar tu problema, ¡es un verdadero dolor de cabeza!

Swift 3.0

(Ver historial de edición para otras versiones de Swift)

Para una implementación más agradable como una extensión para UIWindowpermitir que se pase una transición opcional.

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

Uso:

window.set(rootViewController: viewController)

O

let transition = CATransition()
transition.type = kCATransitionFade
window.set(rootViewController: viewController, withTransition: transition)
Rico
fuente
6
Gracias. Funcionó. Por favor comparta si encuentra un mejor enfoque
Jamal Zafar
8
Parece que reemplazar un controlador de vista raíz que ha presentado vistas (o intentar desasignar una UIWindow que aún presenta controladores de vista) resultará en una pérdida de memoria. Me parece que presentar un controlador de vista crea un bucle de retención con la ventana, y descartar los controladores es la única forma que he encontrado para romperlo. Creo que algunos bloques de terminación internos tienen una fuerte referencia a la ventana.
Carl Lindberg
Tuve un problema con NSClassFromString ("UITransitionView") después de convertir a
Swift
También sigue sucediendo en iOS 9 :( También he actualizado para Swift 2.0
Rich
1
@ user023 ¡He usado esta solución exacta en 2 o 3 aplicaciones enviadas a la App Store sin ningún problema! Supongo que como solo está comprobando el tipo de clase con una cadena, está bien (podría ser cualquier cadena). Lo que podría causar un rechazo es tener una clase nombrada UITransitionViewen su aplicación como parte de los símbolos de la aplicación que creo que la App Store usa para verificar.
Rich
5

Enfrenté este problema y me molestó durante todo un día. Probé la solución obj-c de @ Rich y resulta que cuando quiera presentar otro viewController después de eso, seré bloqueado con un UITransitionView en blanco.

Finalmente, lo descubrí de esta manera y funcionó para mí.

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

Muy bien, ahora todo lo que tienes que hacer es llamar [self setRootViewController:newViewController];cuando quieras cambiar el controlador de vista raíz.

Longfei Wu
fuente
Funciona bien, pero hay un destello molesto del controlador de vista de presentación justo antes de que se active el controlador de vista raíz. Animar las dismissViewControllerAnimated:miradas quizás sea un poco mejor que no tener animación. Sin UITransitionViewembargo, evita los fantasmas en la jerarquía de vistas.
pkamb
5

Intento algo simple que funciona para mí en iOs 9.3: simplemente elimine la vista anterior de viewController de su jerarquía durante la dismissViewControllerAnimatedfinalización.

Trabajemos en la vista X, Y y Z como explica benzado :

Es decir, esta secuencia de operaciones ...

  1. X se convierte en controlador de vista raíz
  2. X presenta Y, por lo que la vista de Y está en pantalla
  3. Uso de transitionWithView: para hacer de Z el nuevo controlador de vista raíz

Que dan:

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

En mi caso, X e Y están bien repartidos y su punto de vista ya no está en jerarquía.

gbitaudeau
fuente
0

Tuve un problema similar. En mi caso, tenía una jerarquía de viewController, y uno de los controladores de vista secundarios tenía un controlador de vista presentado. Cuando cambié el controlador de vista raíz de Windows, por alguna razón, el controlador de vista presentado todavía estaba en la memoria. Entonces, la solución fue descartar todos los controladores de vista antes de cambiar el controlador de vista raíz de Windows.

Robert Fogash
fuente
-2

Llegué a este problema al usar este código:

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

    })
}

Al deshabilitar este código, se solucionó el problema. Logré que esto funcionara habilitando solo esta animación de transición cuando se inicializa la barra de filtros que se anima.

No es realmente la respuesta que está buscando, pero podría llevarlo a la plataforma adecuada para encontrar la solución.

Antoine
fuente