Undichte Ansichten beim Ändern von rootViewController in TransitionWithView

97

Bei der Untersuchung eines Speicherverlusts entdeckte ich ein Problem im Zusammenhang mit der Technik des Aufrufs setRootViewController:in einem Übergangsanimationsblock:

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

Wenn der alte Ansichtscontroller (der zu ersetzende) derzeit einen anderen Ansichtscontroller anzeigt, entfernt der obige Code die dargestellte Ansicht nicht aus der Ansichtshierarchie.

Das heißt, diese Abfolge von Operationen ...

  1. X wird zum Root View Controller
  2. X zeigt Y an, sodass die Ansicht von Y auf dem Bildschirm angezeigt wird
  3. Verwenden Sie transitionWithView:, um Z zum neuen Root View Controller zu machen

... sieht für den Benutzer in Ordnung aus, aber das Debug View Hierarchy-Tool zeigt an, dass die Ansicht von Y immer noch hinter der Ansicht von Z in a vorhanden ist UITransitionView. Das heißt, nach den drei obigen Schritten lautet die Ansichtshierarchie:

  • UIWindow
    • UITransitionView
      • UIView (Y's Ansicht)
    • UIView (Zs Ansicht)

Ich vermute, dass dies ein Problem ist, da die Ansicht von X zum Zeitpunkt des Übergangs nicht Teil der Ansichtshierarchie ist.

Wenn ich dismissViewControllerAnimated:NOunmittelbar zuvor an X sende , transitionWithView:lautet die resultierende Ansichtshierarchie:

  • UIWindow
    • UIView (X's Ansicht)
    • UIView (Zs Ansicht)

Wenn ich dismissViewControllerAnimated:(JA oder NEIN) an X sende und dann den Übergang im completion:Block durchführe, ist die Ansichtshierarchie korrekt. Leider stört das die Animation. Wenn die Entlassung animiert wird, wird Zeit verschwendet. Wenn es nicht animiert, sieht es kaputt aus.

Ich versuche einige andere Ansätze (z. B. eine neue Container-View-Controller-Klasse als Root-View-Controller zu erstellen), habe aber nichts gefunden, was funktioniert. Ich werde diese Frage aktualisieren, wenn ich gehe.

Das ultimative Ziel ist der direkte Übergang von der dargestellten Ansicht zu einem neuen Root-Ansichts-Controller, ohne dass hier Streuansichtshierarchien verbleiben.

Benzado
quelle
Ich habe das gleiche Problem derzeit
Alex
Ich hatte gerade das gleiche Problem
Jamal Zafar
Haben Sie Glück, eine vernünftige Lösung dafür zu finden? Gleiches genaues Problem hier.
David Baez
@DavidBaez Ich habe Code geschrieben, um alle View-Controller vor dem Ändern des Stammverzeichnisses aggressiv zu schließen. Es ist jedoch sehr spezifisch für meine App. Seit ich dies gepostet habe, habe ich mich gefragt, ob das Tauschen das UIWindowRichtige ist, aber ich hatte nicht die Zeit, viel zu experimentieren.
Benzado

Antworten:

119

Ich hatte kürzlich ein ähnliches Problem. Ich musste das manuell UITransitionViewaus dem Fenster entfernen, um das Problem zu beheben, und dann auf dem vorherigen Root-View-Controller "Entlassen" aufrufen, um sicherzustellen, dass die Zuordnung aufgehoben wurde.

Das Update ist nicht wirklich sehr schön, aber es sei denn, Sie haben seit dem Posten der Frage einen besseren Weg gefunden, es ist das einzige, was ich gefunden habe, um zu funktionieren! viewControllerist nur das newControllervon Ihrer ursprünglichen Frage.

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

Ich hoffe, das hilft dir auch, dein Problem zu beheben, es ist ein absoluter Schmerz im Arsch!

Swift 3.0

(Weitere Swift-Versionen finden Sie im Bearbeitungsverlauf.)

Für eine schönere Implementierung als Erweiterung für die UIWindowÜbergabe eines optionalen Übergangs.

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

Verwendung:

window.set(rootViewController: viewController)

Oder

let transition = CATransition()
transition.type = kCATransitionFade
window.set(rootViewController: viewController, withTransition: transition)
Reich
quelle
6
Vielen Dank. Es funktionierte. Bitte teilen Sie, wenn Sie einen besseren Ansatz finden
Jamal Zafar
8
Es scheint, dass das Ersetzen eines Root-View-Controllers, der Ansichten präsentiert hat (oder der Versuch, die Zuordnung eines UIWindow aufzuheben, das noch View-Controller präsentiert hat) zu einem Speicherverlust führt. Es scheint mir, dass das Präsentieren eines Ansichts-Controllers eine Aufbewahrungsschleife mit dem Fenster erzeugt, und das Löschen der Controller ist der einzige Weg, den ich gefunden habe, um es zu brechen. Ich denke, einige interne Abschlussblöcke haben einen starken Bezug zum Fenster.
Carl Lindberg
Hatte ein Problem mit NSClassFromString ("UITransitionView") nach der Konvertierung in Swift 2.0
Eugene Braginets
Auch in iOS 9 passiert immer noch :( Außerdem habe ich für Swift 2.0
Rich
1
@ user023 Ich habe genau diese Lösung in 2 oder 3 Apps verwendet, die ohne Probleme an den App Store gesendet wurden! Ich denke, da Sie nur den Typ der Klasse anhand einer Zeichenfolge überprüfen, ist dies in Ordnung (es kann sich um eine beliebige Zeichenfolge handeln). Was zu einer Ablehnung führen kann, ist die Bezeichnung einer Klasse UITransitionViewin Ihrer App, die dann als Teil der Symbole der App erfasst wird, die meiner Meinung nach vom App Store überprüft werden.
Rich
5

Ich war mit diesem Problem konfrontiert und es hat mich einen ganzen Tag lang geärgert. Ich habe @ Richs obj-c-Lösung ausprobiert und es stellt sich heraus, dass ich mit einer leeren UITransitionView blockiert werde, wenn ich danach einen anderen viewController präsentieren möchte.

Schließlich habe ich es so herausgefunden und es hat bei mir funktioniert.

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

Okay, jetzt müssen Sie nur noch anrufen, [self setRootViewController:newViewController];wenn Sie den Root View Controller wechseln möchten.

Longfei Wu
quelle
Funktioniert gut, aber es gibt einen nervigen Blitz des präsentierenden Ansichts-Controllers, kurz bevor der Root-Ansichts-Controller eingeschaltet wird. Das Animieren dismissViewControllerAnimated:sieht vielleicht etwas besser aus als keine Animation. Vermeidet jedoch die Geister UITransitionViewin der Ansichtshierarchie.
pkamb
5

Ich versuche eine einfache Sache, die für mich unter iOs 9.3 funktioniert: Entfernen Sie einfach die Ansicht des alten viewControllers während des dismissViewControllerAnimatedAbschlusses aus seiner Hierarchie .

Lassen Sie uns an der X-, Y- und Z-Ansicht arbeiten, wie von Benzado erklärt :

Das heißt, diese Abfolge von Operationen ...

  1. X wird zum Root View Controller
  2. X zeigt Y an, sodass die Ansicht von Y auf dem Bildschirm angezeigt wird
  3. Verwenden von TransitionWithView: um Z zum neuen Root View Controller zu machen

Welche geben:

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

In meinem Fall sind X und Y gut freigegeben und ihre Ansicht ist nicht mehr hierarchisch!

gbitaudeau
quelle
0

Hatte ein ähnliches Problem. In meinem Fall hatte ich eine viewController-Hierarchie, und einer der untergeordneten View-Controller hatte einen präsentierten View-Controller. Als ich dann den Windows-Root-View-Controller änderte, befand sich der vorgestellte View-Controller aus irgendeinem Grund noch im Speicher. Die Lösung bestand also darin, alle View-Controller zu schließen, bevor ich den Windows-Root-View-Controller ändere.

Robert Fogash
quelle
-2

Bei der Verwendung dieses Codes bin ich auf dieses Problem gestoßen:

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

    })
}

Durch Deaktivieren dieses Codes wurde das Problem behoben. Ich habe es geschafft, dies zum Laufen zu bringen, indem ich diese Übergangsanimation nur aktiviert habe, wenn die Filterleiste, die animiert wird, initialisiert ist.

Es ist nicht wirklich die Antwort, nach der Sie suchen, aber es könnte Sie auf den richtigen Weg bringen, um Ihre Lösung zu finden.

Antoine
quelle