transitionWithView 내에서 rootViewController를 변경할 때 뷰 누수


97

메모리 누수를 조사하는 동안 setRootViewController:전환 애니메이션 블록 내부에서 호출하는 기술과 관련된 문제를 발견했습니다 .

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

이전 뷰 컨트롤러 (교체중인 뷰 컨트롤러)가 현재 다른 뷰 컨트롤러를 제공하는 경우 위 코드는 뷰 계층에서 제시된 뷰를 제거하지 않습니다.

즉,이 일련의 작업은 ...

  1. X가 루트 뷰 컨트롤러가 됨
  2. X는 Y를 표시하므로 Y의 뷰가 화면에 표시됩니다.
  3. transitionWithView:Z를 새로운 루트 뷰 컨트롤러로 만들기 위해 사용

... 사용자에게는 괜찮아 보이지만 Debug View Hierarchy 도구는 Y의 뷰가 여전히 Z의 뷰 뒤에 있음을 보여줍니다 UITransitionView. 즉, 위의 세 단계 후 뷰 계층 구조는 다음과 같습니다.

  • UIWindow
    • UITransitionView
      • UIView (Y 's view)
    • UIView (Z 's view)

전환 시점에 X의 뷰가 실제로 뷰 계층 구조의 일부가 아니기 때문에 이것이 문제라고 생각합니다.

dismissViewControllerAnimated:NO바로 전에 X로 보내면 transitionWithView:결과 뷰 계층 구조는 다음과 같습니다.

  • UIWindow
    • UIView (X의 관점)
    • UIView (Z 's view)

dismissViewControllerAnimated:X에 (YES 또는 NO)를 보내고 completion:블록 에서 전환을 수행 하면 뷰 계층 구조가 정확합니다. 불행히도 그것은 애니메이션을 방해합니다. 해고를 애니메이션화하면 시간이 낭비됩니다. 애니메이션하지 않으면 깨져 보입니다.

다른 접근 방식 (예 : 루트 뷰 컨트롤러 역할을하는 새 컨테이너 뷰 컨트롤러 클래스 만들기)을 시도하고 있지만 작동하는 것을 찾지 못했습니다. 이 질문을 업데이트하겠습니다.

궁극적 인 목표는 제시된 뷰에서 새로운 루트 뷰 컨트롤러로 직접 전환하는 것입니다.


나는 현재이 같은 문제가
알렉스

난 그냥 같은 문제에 직면
자말 자파르

이것에 대한 적절한 해결책을 찾는 행운이 있습니까? 여기에도 똑같은 문제가 있습니다.
David Baez

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

나는 이것이 당신의 문제를 해결하는 데 도움이되기를 바랍니다. 그것은 엉덩이에 절대적인 고통입니다!

스위프트 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
감사. 효과가있었습니다. 더 나은 접근 방식을 찾으면 공유하십시오
Jamal Zafar 2014

8
뷰를 표시 한 루트 뷰 컨트롤러를 교체하거나 뷰 컨트롤러를 표시 한 UIWindow를 할당 해제하려고하면 메모리 누수가 발생하는 것으로 보입니다. 뷰 컨트롤러를 제시하면 창에 리테이너 루프가 생성되고 컨트롤러를 해제하는 것이 내가 그것을 깨는 유일한 방법 인 것 같습니다. 일부 내부 완료 블록에는 창에 대한 강력한 참조가 있다고 생각합니다.
Carl Lindberg 2015

swift 2.0으로 변환 한 후 NSClassFromString ( "UITransitionView")에 문제가 발생했습니다
Eugene Braginets

iOS 9에서도 여전히 발생하고 있습니다. (또한 Swift 2.0
Rich

1
@ user023 문제없이 App Store에 제출 된 2 ~ 3 개의 앱에서이 정확한 솔루션을 사용했습니다! 나는 당신이 문자열에 대해 클래스의 유형을 확인하고 있기 때문에 괜찮습니다 (어떤 문자열이 될 수 있습니다). 거부를 유발할 수있는 것은 UITransitionView앱에 이름이 지정된 클래스가있는 것입니다.이 클래스 는 App Store에서 확인하는 데 사용하는 앱 기호의 일부로 선택됩니다.
Rich

5

나는이 문제에 직면했고 하루 종일 나를 짜증나게했다. @Rich의 obj-c 솔루션을 시도했지만 그 후에 다른 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에서 나를 위해 일하는 간단한 일을 시도합니다 dismissViewControllerAnimated. 완료 하는 동안 계층 구조에서 이전 viewController의 뷰를 제거하기 만하면 됩니다.

benzado가 설명하는대로 X, Y 및 Z보기에서 작업 해 보겠습니다 .

즉,이 일련의 작업은 ...

  1. X가 루트 뷰 컨트롤러가 됨
  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가 잘 할당되고 그들의 뷰가 더 이상 계층 구조에 없습니다!


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

    })
}

이 코드를 비활성화하면 문제가 해결되었습니다. 애니메이션되는 필터 바가 초기화 될 때만이 전환 애니메이션을 활성화하여이 작업을 수행했습니다.

실제로 찾고있는 답은 아니지만 솔루션을 찾는 데 적합한 패드를 찾을 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.