Przeciekające widoki podczas zmiany rootViewController w TransitionWithView

97

Badając wyciek pamięci, odkryłem problem związany z techniką wywoływania setRootViewController:wewnątrz bloku animacji przejścia:

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

Jeśli stary kontroler widoku (zastępowany) aktualnie prezentuje inny kontroler widoku, to powyższy kod nie usuwa prezentowanego widoku z hierarchii widoków.

Oznacza to, że ta sekwencja operacji ...

  1. X staje się głównym kontrolerem widoku
  2. X przedstawia Y, więc widok Y jest na ekranie
  3. Użycie transitionWithView:do uczynienia Z nowego głównego kontrolera widoku

... wygląda dobrze dla użytkownika, ale narzędzie Debug View Hierarchy ujawni, że widok Y nadal znajduje się za widokiem Z, wewnątrz a UITransitionView. Oznacza to, że po wykonaniu trzech powyższych kroków hierarchia widoków wygląda następująco:

  • UIWindow
    • UITransitionView
      • UIView (widok Y)
    • UIView (widok Z)

Podejrzewam, że jest to problem, ponieważ w momencie przejścia widok X nie jest w rzeczywistości częścią hierarchii widoków.

Jeśli wyślę dismissViewControllerAnimated:NOdo X bezpośrednio wcześniej transitionWithView:, wynikowa hierarchia widoków jest następująca:

  • UIWindow
    • UIView (widok X)
    • UIView (widok Z)

Jeśli wyślę dismissViewControllerAnimated:(TAK lub NIE) do X, a następnie wykonam przejście w completion:bloku, to hierarchia widoków jest poprawna. Niestety to przeszkadza w animacji. Jeśli animujesz zwolnienie, marnuje czas; jeśli nie jest animowany, wygląda na zepsuty.

Próbuję innych podejść (np. Tworząc nową klasę kontrolera widoku kontenera, która będzie służyć jako mój główny kontroler widoku), ale nie znalazłem niczego, co działa. Zaktualizuję to pytanie na bieżąco.

Ostatecznym celem jest bezpośrednie przejście z przedstawionego widoku do nowego głównego kontrolera widoku, bez pozostawiania zbłąkanych hierarchii widoków.

benzado
źródło
Obecnie mam ten sam problem
Alex
Właśnie stanąłem przed tym samym problemem
Jamal Zafar
Jakieś szczęście w znalezieniu przyzwoitego rozwiązania tego problemu? Ten sam DOKŁADNY problem tutaj.
David Baez
@DavidBaez Skończyłem pisać kod, aby agresywnie odrzucać wszystkie kontrolery widoku przed zmianą katalogu głównego. Jest to jednak bardzo specyficzne dla mojej aplikacji. Odkąd to opublikowałem, zastanawiałem się, czy zamiana UIWindowjest rzeczą do zrobienia, ale nie miałem czasu na wiele eksperymentów.
benzado

Odpowiedzi:

119

Niedawno miałem podobny problem. Musiałem ręcznie usunąć to UITransitionViewz okna, aby naprawić problem, a następnie wywołać odrzucenie na poprzednim kontrolerze widoku głównego, aby upewnić się, że został zwolniony.

Poprawka nie jest zbyt przyjemna, ale jeśli nie znalazłeś lepszego sposobu od czasu wysłania pytania, to jedyna rzecz, którą znalazłem, która działa! viewControllerto tylko newControllerz twojego pierwotnego pytania.

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

Mam nadzieję, że to również pomoże ci rozwiązać twój problem, to absolutny ból w dupie!

Swift 3.0

(Zobacz historię edycji innych wersji Swift)

Dla ładniejszej implementacji jako rozszerzenia UIWindowumożliwiającego przekazanie opcjonalnego przejścia.

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

Stosowanie:

window.set(rootViewController: viewController)

Lub

let transition = CATransition()
transition.type = kCATransitionFade
window.set(rootViewController: viewController, withTransition: transition)
Bogaty
źródło
6
Dzięki. Zadziałało. Udostępnij, jeśli znajdziesz lepsze podejście
Jamal Zafar,
8
Wygląda na to, że zastąpienie głównego kontrolera widoku, który przedstawił widoki (lub próba cofnięcia alokacji UIWindow, które nadal wyświetlało kontrolery widoku) spowoduje wyciek pamięci. Wydaje mi się, że prezentacja kontrolera widoku tworzy pętlę zachowania z oknem, a odrzucenie kontrolerów jest jedynym sposobem, w jaki znalazłem to przerwanie. Myślę, że niektóre wewnętrzne bloki uzupełniania mają silne odniesienie do okna.
Carl Lindberg
Wystąpił problem z NSClassFromString ("UITransitionView") po konwersji do Swift 2.0
Eugene Braginets
Nadal dzieje się to również w iOS 9 :( Również zaktualizowałem do Swift 2.0
Rich
1
@ user023 Użyłem dokładnie tego rozwiązania w 2 lub 3 aplikacjach przesłanych do App Store bez problemu! Wydaje mi się, że ponieważ sprawdzasz tylko typ klasy w ciągu znaków, to jest w porządku (może to być dowolny ciąg). To, co może spowodować odrzucenie, to posiadanie klasy o nazwie UITransitionVieww Twojej aplikacji, która jest wtedy pobierana jako część symboli aplikacji, które, jak sądzę, używa App Store do sprawdzenia.
Rich
5

Zmierzyłem się z tym problemem i denerwował mnie przez cały dzień. Wypróbowałem rozwiązanie obj-c @ Richa i okazuje się, że kiedy chcę przedstawić inny viewController po tym, zostanie zablokowany pustym UITransitionView.

W końcu doszedłem do tego w jaki sposób i to zadziałało.

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

W porządku, teraz wszystko, co musisz zrobić, to zadzwonić, [self setRootViewController:newViewController];gdy chcesz przełączyć kontroler widoku głównego.

Longfei Wu
źródło
Działa dobrze, ale tuż przed włączeniem głównego kontrolera widoku pojawia się irytujący błysk kontrolera widoku prezentacji. Animowanie dismissViewControllerAnimated:wyglądu może być nieco lepsze niż brak animacji. UITransitionViewJednak unika duchów w hierarchii widoku.
pkamb
5

Próbuję prostej rzeczy, która działa dla mnie na iOs 9.3: po prostu usuń stary widok ViewControllera z jego hierarchii podczas dismissViewControllerAnimatedkończenia pracy.

Popracujmy nad widokiem X, Y i Z, jak wyjaśniono w benzado :

Oznacza to, że ta sekwencja operacji ...

  1. X staje się głównym kontrolerem widoku
  2. X przedstawia Y, więc widok Y jest na ekranie
  3. Korzystanie z transitWithView: aby uczynić Z nowego głównego kontrolera widoku

Które dają:

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

W moim przypadku X i Y są dobrze zwolnione, a ich widok nie jest już w hierarchii!

gbitaudeau
źródło
0

Miałem podobny problem. W moim przypadku miałem hierarchię viewController, a jeden z podrzędnych kontrolerów widoku miał przedstawiony kontroler widoku. Kiedy zmieniłem wtedy kontroler widoku głównego systemu Windows, z jakiegoś powodu prezentowany kontroler widoku wciąż był w pamięci. Tak więc rozwiązaniem było odrzucenie wszystkich kontrolerów widoku przed zmianą kontrolera widoku głównego systemu Windows.

Robert Fogash
źródło
-2

Do tego problemu doszedłem używając tego kodu:

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

    })
}

Wyłączenie tego kodu rozwiązało problem. Udało mi się to uruchomić, włączając animację przejścia tylko wtedy, gdy pasek filtru, który jest animowany, jest inicjowany.

Tak naprawdę nie jest to odpowiedź, której szukasz, ale może przynieść ci właściwą podkładkę do znalezienia rozwiązania.

Antoine
źródło