Витік поглядів при зміні rootViewController всередині переходуWithView


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 новим контролером кореневого перегляду

... для користувача виглядає нормально, але інструмент ієрархії вигляду налагодження покаже, що вид Y все ще знаходиться позаду зору Z, всередині a UITransitionView. Тобто, після трьох кроків вище, ієрархія подання така:

  • UIWindow
    • UITransitionView
      • UIView (погляд Y)
    • UIView (подання Z)

Я підозрюю, що це проблема, оскільки на момент переходу подання X насправді не є частиною ієрархії подання.

Якщо я відправлю dismissViewControllerAnimated:NOв X безпосередньо перед цим transitionWithView:, отримана ієрархія подання:

  • UIWindow
    • UIView (вид X)
    • UIView (подання Z)

Якщо я посилаю dismissViewControllerAnimated:(ТАК чи НІ) на X, то виконую перехід у completion:блоці, тоді ієрархія подання є правильною. На жаль, це заважає анімації. Якщо оживляє звільнення, це марно витрачає час; якщо не анімувати, то виглядає зламаним.

Я пробую деякі інші підходи (наприклад, створюю новий клас контролера подання контейнера, який би служив моїм контролером кореневого перегляду), але не знайшов нічого, що працює. Я буду оновлювати це питання, коли ходжу.

Кінцевою метою є перехід від представленого подання до нового контролера кореневого перегляду безпосередньо, не залишаючи ієрархічних поглядів навколо.


Зараз у мене така сама проблема
Алекс

Я щойно зіткнувся з тим самим питанням
Джамал Зафар

Пощастило знайти гідне рішення цього? Ту саму ТОЧНУ проблему тут.
Девід Баез,

@DavidBaez Я завершив писати код, щоб агресивно відхиляти всі контролери перегляду перед тим, як змінити корінь. Однак це дуже специфічно для мого додатка. З моменту публікації цього запитання мені було цікаво, чи UIWindowне потрібно робити обмін , але не було часу багато експериментувати.
benzado

Відповіді:


119

Нещодавно у мене була подібна проблема. Мені довелося вручну видалити це UITransitionViewз вікна, щоб вирішити проблему, а потім викликати dismiss на попередньому контролері кореневого перегляду, щоб переконатися, що його було виведено.

Виправлення насправді не дуже приємне, але якщо ви не знайшли кращого способу після опублікування запитання, це єдине, що я знайшов, що працює! 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
Схоже, що заміна кореневого контролера подання, який представив подання (або спроба розблокувати вікно інтерфейсу користувача, яке все ще представляло контролери подання), призведе до витоку пам'яті. Мені здається, що представлення контролера подання створює цикл збереження з вікном, і відхилення контролерів - це єдиний спосіб, який я знайшов, щоб його зламати. Я думаю, що деякі внутрішні блоки завершення мають сильне посилання на вікно.
Карл Ліндберг,

Виникла проблема з NSClassFromString ("UITransitionView") після переходу на swift 2.0
Євген Брагінець

Це все ще відбувається в iOS 9 :( Також я оновлював Swift 2.0
Rich

1
@ user023 Я використовував саме це рішення в 2 або 3 програмах, поданих в App Store без проблем! Я думаю, оскільки ви перевіряєте лише тип класу щодо рядка, це нормально (це може бути будь-який рядок). Що може спричинити відмову, так це наявність UITransitionViewу вашому додатку класу з іменем , який тоді було вибрано як частина символів програми, яку, на мою думку, App Store використовує для перевірки.
Багатий

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. Використання переходуWithView: щоб зробити 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 добре розправляються, і їх погляд більше не відповідає ієрархії!


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

    })
}

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

Це насправді не відповідь, яку ви шукаєте, але вона може допомогти вам знайти потрібне рішення.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.