Perdita di visualizzazioni quando si cambia rootViewController all'interno di transactionWithView


97

Durante le indagini su una perdita di memoria ho scoperto un problema relativo alla tecnica di chiamata setRootViewController:all'interno di un blocco di animazione di transizione:

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

Se il vecchio controller di visualizzazione (quello che viene sostituito) presenta attualmente un altro controller di visualizzazione, il codice precedente non rimuove la visualizzazione presentata dalla gerarchia della visualizzazione.

Cioè, questa sequenza di operazioni ...

  1. X diventa Root View Controller
  2. X presenta Y, in modo che la vista di Y sia sullo schermo
  3. Usando transitionWithView:per rendere Z il nuovo Root View Controller

... sembra OK all'utente, ma lo strumento Debug View Hierarchy rivelerà che la vista di Y è ancora lì dietro la vista di Z, all'interno di un file UITransitionView. Cioè, dopo i tre passaggi precedenti, la gerarchia di visualizzazione è:

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

Sospetto che questo sia un problema perché, al momento della transizione, la vista di X non fa effettivamente parte della gerarchia della vista.

Se invio dismissViewControllerAnimated:NOa X immediatamente prima transitionWithView:, la gerarchia di visualizzazione risultante è:

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

Se invio dismissViewControllerAnimated:(SÌ o NO) a X, quindi eseguo la transizione nel completion:blocco, la gerarchia della vista è corretta. Sfortunatamente, ciò interferisce con l'animazione. Se anima il licenziamento, fa perdere tempo; se non si anima, sembra rotto.

Sto provando altri approcci (ad esempio, creare una nuova classe di controller di visualizzazione del contenitore che funga da controller di visualizzazione di root) ma non ho trovato nulla che funzioni. Aggiornerò questa domanda mentre vado.

L'obiettivo finale è passare direttamente dalla vista presentata a un nuovo controller della vista principale e senza lasciare in giro gerarchie di viste vaganti.


Al momento ho lo stesso problema
Alex

Ho appena affrontato lo stesso problema
Jamal Zafar

Hai avuto fortuna nel trovare una soluzione decente a questo? Lo stesso ESATTO problema qui.
David Baez

@DavidBaez Ho finito per scrivere codice per chiudere in modo aggressivo tutti i controller di visualizzazione prima di cambiare la radice. Tuttavia, è molto specifico per la mia app. Da quando ho pubblicato questo articolo mi sono chiesto se lo scambio UIWindowfosse la cosa da fare, ma non ho avuto il tempo di sperimentare molto.
benzado

Risposte:


119

Ho avuto un problema simile di recente. Ho dovuto rimuoverlo manualmente UITransitionViewdalla finestra per risolvere il problema, quindi chiamare dismiss sul precedente controller di visualizzazione root per assicurarmi che fosse deallocato.

La correzione non è davvero molto bella ma, a meno che tu non abbia trovato un modo migliore dopo aver pubblicato la domanda, è l'unica cosa che ho trovato che funzioni! viewControllerè solo il newControllerdalla tua domanda originale.

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

Spero che questo ti aiuti a risolvere anche il tuo problema, è un vero rompicoglioni!

Swift 3.0

(Vedi la cronologia delle modifiche per altre versioni di Swift)

Per un'implementazione migliore come estensione per UIWindowconsentire il passaggio di una transizione opzionale.

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

Utilizzo:

window.set(rootViewController: viewController)

O

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

6
Grazie. Ha funzionato. Per favore condividi se trovi un approccio migliore
Jamal Zafar

8
Sembra che la sostituzione di un controller di visualizzazione radice che ha presentato visualizzazioni (o il tentativo di deallocare una finestra UIWindow che ha ancora presentato controller di visualizzazione) comporterà una perdita di memoria. Mi sembra che la presentazione di un controller di visualizzazione crei un loop di conservazione con la finestra e chiudere i controller è l'unico modo che ho trovato per interromperlo. Penso che alcuni blocchi di completamento interni abbiano un forte riferimento alla finestra.
Carl Lindberg

Si è verificato un problema con NSClassFromString ("UITransitionView") dopo la conversione a swift 2.0
Eugene Braginets,

Succede ancora anche in iOS 9 :( Inoltre ho aggiornato per Swift 2.0
Rich

1
@ user023 Ho utilizzato questa soluzione esatta in 2 o 3 app inviate all'App Store senza problemi! Immagino che poiché stai controllando solo il tipo della classe rispetto a una stringa, va bene (potrebbe essere qualsiasi stringa). Ciò che potrebbe causare un rifiuto è avere una classe denominata UITransitionViewnella tua app che viene raccolta come parte dei simboli dell'app che penso che l'App Store usi per controllare.
Rich

5

Ho affrontato questo problema e mi ha infastidito per un'intera giornata. Ho provato la soluzione obj-c di @ Rich e si scopre che quando voglio presentare un altro viewController dopo, verrò bloccato con un UITransitionView vuoto.

Alla fine ho capito in questo modo e ha funzionato per me.

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

Va bene, ora tutto ciò che devi fare è chiamare [self setRootViewController:newViewController];quando vuoi cambiare il controller di visualizzazione di root.


Funziona bene, ma c'è un fastidioso lampo del controller della vista di presentazione subito prima che venga attivato il controller della vista principale. Animare l' dismissViewControllerAnimated:aspetto forse è un po 'meglio di nessuna animazione. Evita però i fantasmi UITransitionViewnella gerarchia della vista.
pkamb

5

Provo una cosa semplice che funziona per me su iOs 9.3: rimuovi la vista del vecchio viewController dalla sua gerarchia durante il dismissViewControllerAnimatedcompletamento.

Lavoriamo sulle viste X, Y e Z come spiegato da benzado :

Cioè, questa sequenza di operazioni ...

  1. X diventa Root View Controller
  2. X presenta Y, in modo che la vista di Y sia sullo schermo
  3. Utilizzo di TransitionWithView: per rendere Z il nuovo Root View Controller

Che danno:

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

Nel mio caso, X e Y sono ben deallocati e il loro punto di vista non è più in gerarchia!


0

Ho avuto un problema simile. Nel mio caso avevo una gerarchia viewController e uno dei controller di visualizzazione figlio aveva un controller di visualizzazione presentato. Quando ho cambiato il controller della vista root di Windows, per qualche motivo, il controller della vista presentato era ancora nella memoria. Quindi, la soluzione era chiudere tutti i controller di visualizzazione prima di cambiare il controller di visualizzazione root di Windows.


-2

Sono arrivato a questo problema usando questo codice:

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

    })
}

Disabilitando questo codice, risolto il problema. Sono riuscito a farlo funzionare abilitando questa animazione di transizione solo quando la barra dei filtri che si anima viene inizializzata.

Non è proprio la risposta che stai cercando, ma potrebbe portarti sul pad giusto per trovare la tua soluzione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.