Vazamento de visualizações ao alterar rootViewController dentro da transiçãoWithView

97

Ao investigar um vazamento de memória, descobri um problema relacionado à técnica de chamada setRootViewController:dentro de um bloco de animação de transição:

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

Se o controlador de visão antigo (o que está sendo substituído) está apresentando outro controlador de visão, o código acima não remove a visão apresentada da hierarquia de visão.

Ou seja, esta sequência de operações ...

  1. X torna-se Controlador de Visualização Root
  2. X apresenta Y, de modo que a visão de Y esteja na tela
  3. Usando transitionWithView:para fazer de Z o novo controlador de visualização raiz

... parece OK para o usuário, mas a ferramenta Debug View Hierarchy revelará que a visão de Y ainda está lá atrás da visão de Z, dentro de a UITransitionView. Ou seja, após as três etapas acima, a hierarquia de visualização é:

  • UIWindow
    • UITransitionView
      • UIView (visualização de Y)
    • UIView (visão de Z)

Suspeito que isso seja um problema porque, no momento da transição, a visualização de X não faz parte da hierarquia de visualizações.

Se eu enviar dismissViewControllerAnimated:NOpara X imediatamente antes transitionWithView:, a hierarquia de visualização resultante será:

  • UIWindow
    • UIView (visualização de X)
    • UIView (visão de Z)

Se eu enviar dismissViewControllerAnimated:(SIM ou NÃO) para X, então faço a transição no completion:bloco, então a hierarquia de visualização está correta. Infelizmente, isso interfere na animação. Se animar a dispensa, é uma perda de tempo; se não estiver animando, parece quebrado.

Estou tentando algumas outras abordagens (por exemplo, fazer uma nova classe de controlador de visualização de contêiner para servir como meu controlador de visualização raiz), mas não encontrei nada que funcione. Vou atualizar esta pergunta à medida que avançar.

O objetivo final é fazer a transição da visão apresentada para um novo controlador de visão raiz diretamente e sem deixar hierarquias de visão perdidas ao redor.

benzado
fonte
Eu tenho esse mesmo problema atualmente
Alex
Acabei de enfrentar o mesmo problema
Jamal Zafar
Teve sorte em encontrar uma solução decente para isso? O mesmo problema EXATO aqui.
David Baez
@DavidBaez Acabei escrevendo código para dispensar agressivamente todos os controladores de visualização antes de alterar a raiz. No entanto, é muito específico para meu aplicativo. Desde a postagem disso, tenho me perguntado se trocar o UIWindowé a coisa certa a fazer, mas não tive tempo de experimentar muito.
benzado

Respostas:

119

Tive um problema semelhante recentemente. Tive que removê-lo manualmente UITransitionViewda janela para corrigir o problema e, em seguida, chamar dispense no controlador de visualização root anterior para garantir que fosse desalocado.

A correção não é realmente muito boa, mas a menos que você tenha encontrado uma maneira melhor desde a postagem da pergunta, é a única coisa que descobri que funciona! viewControlleré apenas o newControllerda sua pergunta 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 isso ajude você a resolver seu problema também, é um pé no saco!

Swift 3.0

(Veja o histórico de edição para outras versões do Swift)

Para uma implementação mais agradável como uma extensão em UIWindowpermitir que uma transição opcional seja passada.

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)

Ou

let transition = CATransition()
transition.type = kCATransitionFade
window.set(rootViewController: viewController, withTransition: transition)
Rico
fonte
6
Obrigado. Funcionou. Por favor, compartilhe se você encontrar uma abordagem melhor
Jamal Zafar
8
Parece que substituir um controlador de visualização raiz que apresentou visualizações (ou tentar desalocar uma UIWindow que ainda apresentou controladores de visualização) resultará em um vazamento de memória. Parece-me que apresentar um controlador de visualização cria um loop de retenção com a janela, e descartar os controladores é a única maneira que encontrei de quebrá-lo. Acho que alguns blocos de conclusão internos têm uma forte referência à janela.
Carl Lindberg
Tive um problema com NSClassFromString ("UITransitionView") após a conversão para o swift 2.0
Eugene Braginets
Ainda acontecendo no iOS 9 também :( Também atualizei para Swift 2.0
Rich
1
@ user023 Usei essa solução exata em 2 ou 3 aplicativos enviados à App Store sem nenhum problema! Eu acho que como você está apenas verificando o tipo da classe em relação a uma string, tudo bem (pode ser qualquer string). O que pode causar uma rejeição é ter uma classe nomeada UITransitionViewem seu aplicativo, pois é escolhida como parte dos símbolos do aplicativo que eu acho que a App Store usa para verificar.
Rich
5

Enfrentei esse problema e isso me incomodou um dia inteiro. Eu tentei a solução obj-c do @Rich e descobri que quando eu quiser apresentar outro viewController depois disso, serei bloqueado com um UITransitionView em branco.

Finalmente, descobri dessa forma e funcionou para mim.

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

Tudo bem, agora tudo o que você precisa fazer é chamar [self setRootViewController:newViewController];quando quiser mudar o controlador de visualização raiz.

Longfei Wu
fonte
Funciona bem, mas há um flash irritante do controlador de visualização de apresentação logo antes de o controlador de visualização raiz ser ativado. A animação da dismissViewControllerAnimated:aparência talvez seja um pouco melhor do que nenhuma animação. No UITransitionViewentanto, evita os fantasmas na hierarquia de visualizações.
pkamb
5

Eu tento uma coisa simples que funciona para mim no iOs 9.3: apenas remover a visão do antigo viewController de sua hierarquia durante a dismissViewControllerAnimatedconclusão.

Vamos trabalhar nas visualizações X, Y e Z, conforme explicado por benzado :

Ou seja, esta sequência de operações ...

  1. X torna-se Controlador de Visualização Root
  2. X apresenta Y, de modo que a visão de Y esteja na tela
  3. Usando a transiçãoWithView: para tornar Z o novo Controlador de visualização raiz

Que dão:

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

No meu caso, X e Y estão bem desalocados e a visão deles não está mais na hierarquia!

gbitaudeau
fonte
0

Teve um problema semelhante. No meu caso, eu tinha uma hierarquia viewController e um dos controladores de visualização filho tinha um controlador de visualização apresentado. Quando mudei o controlador de visualização do Windows root, por algum motivo, o controlador de visualização apresentado ainda estava na memória. Portanto, a solução foi dispensar todos os controladores de visualização antes de alterar o controlador de visualização raiz do Windows.

Robert Fogash
fonte
-2

Eu cheguei a este problema ao 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

    })
}

Desativar este código corrigiu o problema. Consegui fazer isso funcionar habilitando essa animação de transição apenas quando a barra de filtros que fica animada é inicializada.

Não é realmente a resposta que você está procurando, mas pode levá-lo ao ponto certo para encontrar sua solução.

Antoine
fonte