Утечка представлений при изменении rootViewController внутри transitionWithView

97

При исследовании утечки памяти я обнаружил проблему, связанную с техникой вызова setRootViewController:внутри блока анимации перехода:

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

Если старый контроллер представления (тот, который заменяется) в настоящее время представляет другой контроллер представления, то приведенный выше код не удаляет представленное представление из иерархии представлений.

То есть такая последовательность операций ...

  1. X становится Root View Controller
  2. X представляет Y, так что вид Y находится на экране
  3. Использование, transitionWithView:чтобы сделать Z новым контроллером корневого представления

... выглядит нормально для пользователя, но инструмент Debug View Hierarchy покажет, что представление Y все еще находится за представлением Z, внутри UITransitionView. То есть после трех шагов, описанных выше, иерархия представлений будет следующей:

  • UIWindow
    • UITransitionView
      • UIView (взгляд Y)
    • UIView (взгляд Z)

Я подозреваю, что это проблема, потому что во время перехода представление X фактически не является частью иерархии представлений.

Если я отправлю dismissViewControllerAnimated:NOX непосредственно перед этим transitionWithView:, результирующая иерархия представлений будет:

  • UIWindow
    • UIView (взгляд X)
    • UIView (взгляд Z)

Если я отправлю dismissViewControllerAnimated:(ДА или НЕТ) в X, а затем выполню переход в completion:блоке, тогда иерархия представлений верна. К сожалению, это мешает анимации. Анимация увольнения - пустая трата времени; если не анимировать, он выглядит сломанным.

Я пробую другие подходы (например, создание нового класса контроллера представления контейнера в качестве моего корневого контроллера представления), но не нашел ничего, что работает. Я буду обновлять этот вопрос по мере поступления.

Конечная цель - напрямую перейти от представленного представления к новому контроллеру корневого представления, не оставляя ненужных иерархий представлений.

Benzado
источник
У меня такая же проблема в настоящее время
Alex
Я столкнулся с той же проблемой
Джамал Зафар
Есть ли удача найти достойное решение для этого? Та же ТОЧНАЯ проблема здесь.
Дэвид Баез
@DavidBaez Я закончил писать код, чтобы агрессивно отключать все контроллеры представления перед изменением корня. Однако это очень специфично для моего приложения. С тех пор, как я разместил это, я задавался вопросом, UIWindowстоит ли менять местами , но у меня не было времени на эксперименты.
benzado 01

Ответы:

119

Недавно у меня была похожая проблема. Мне пришлось вручную удалить это UITransitionViewиз окна, чтобы решить проблему, а затем вызвать отклонение на предыдущем контроллере корневого представления, чтобы убедиться, что он освобожден.

Исправление на самом деле не очень хорошее, но если вы не нашли лучший способ с момента публикации вопроса, это единственное, что я нашел работающим! viewControllerэто просто newControllerиз вашего исходного вопроса.

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

Я надеюсь, что это поможет вам решить и вашу проблему, это абсолютная заноза в заднице!

Swift 3.0

(См. Историю изменений для других версий Swift)

Для более удобной реализации в качестве расширения, UIWindowпозволяющего передавать необязательный переход.

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

Использование:

window.set(rootViewController: viewController)

Или

let transition = CATransition()
transition.type = kCATransitionFade
window.set(rootViewController: viewController, withTransition: transition)
Богатый
источник
6
Спасибо. Это сработало. Пожалуйста, поделитесь, если найдете лучший подход
Джамал Зафар
8
Похоже, что замена корневого контроллера представления, который представил представления (или попытка освободить UIWindow, который все еще представил контроллеры представления), приведет к утечке памяти. Мне кажется, что представление контроллера представления создает цикл сохранения с окном, и отключение контроллеров - единственный способ, который я нашел, чтобы сломать его. Я думаю, что некоторые внутренние блоки завершения имеют сильную ссылку на окно.
Карл Линдберг,
Возникла проблема с NSClassFromString ("UITransitionView") после преобразования в Swift 2.0
Евгений Брагинец
Все еще происходит в iOS 9 :( Также я обновился для Swift 2.0
Rich
1
@ user023 Я без проблем использовал это решение в 2 или 3 приложениях, отправленных в App Store! Я думаю, поскольку вы проверяете только тип класса по строке, это нормально (это может быть любая строка). Что может вызвать отклонение, так это наличие класса, названного UITransitionViewв вашем приложении, как тогда, который был выбран как часть символов приложения, которые, я думаю, App Store использует для проверки.
Rich
5

Я столкнулся с этой проблемой, и она меня раздражала целый день. Я пробовал решение obj-c @ Rich, и оказалось, что когда я хочу представить еще один viewController после этого, я буду заблокирован пустым UITransitionView.

Наконец, я понял, что это сработало для меня.

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

Хорошо, теперь все, что вам нужно сделать, это вызвать, [self setRootViewController:newViewController];когда вы хотите переключить контроллер корневого представления.

Лунфэй Ву
источник
Работает хорошо, но прямо перед включением корневого контроллера представления возникает раздражающая вспышка контроллера представления. dismissViewControllerAnimated:Возможно, анимация внешнего вида немного лучше, чем отсутствие анимации. UITransitionViewТем не менее, избегает призраков в иерархии представлений.
pkamb
5

Я пробую простую вещь, которая работает для меня на iOs 9.3: просто удалите старое представление viewController из его иерархии во время dismissViewControllerAnimatedзавершения.

Давайте работать с представлениями X, Y и Z, как объяснил benzado :

То есть такая последовательность операций ...

  1. X становится Root View Controller
  2. X представляет Y, так что вид Y находится на экране
  3. Использование transitionWithView: чтобы сделать Z новым контроллером корневого представления

Которые дают:

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

В моем случае X и Y хорошо освобождены, и их представление больше не находится в иерархии!

Gbitaudeau
источник
0

Была аналогичная проблема. В моем случае у меня была иерархия viewController, и один из дочерних контроллеров представления имел представленный контроллер представления. Когда я изменил контроллер корневого представления Windows, по какой-то причине представленный контроллер представления все еще находился в памяти. Итак, решение заключалось в том, чтобы закрыть все контроллеры представления, прежде чем я изменю контроллер корневого представления Windows.

Роберт Фогаш
источник
-2

Я столкнулся с этой проблемой при использовании этого кода:

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

    })
}

Отключение этого кода решило проблему. Мне удалось заставить это работать, включив эту анимацию перехода только при инициализации анимированной панели фильтров.

На самом деле это не тот ответ, который вы ищете, но он может подвести вас к правильному пути для поиска решения.

Антуан
источник