มุมมองที่รั่วไหลเมื่อเปลี่ยน 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 เป็น Root View Controller ใหม่

... ดูเหมือนผู้ใช้จะตกลง แต่เครื่องมือลำดับชั้นของมุมมองดีบักจะเปิดเผยว่ามุมมองของ Y ยังคงมีอยู่เบื้องหลังมุมมองของ Z ภายใน a UITransitionView. นั่นคือหลังจากสามขั้นตอนข้างต้นลำดับชั้นของมุมมองคือ:

  • UIWindow
    • UITransitionView
      • UIView (มุมมองของ Y)
    • UIView (มุมมองของ Z)

ฉันสงสัยว่านี่เป็นปัญหาเพราะในช่วงเวลาของการเปลี่ยนแปลงมุมมองของ X ไม่ได้เป็นส่วนหนึ่งของลำดับชั้นมุมมอง

ถ้าฉันส่งdismissViewControllerAnimated:NOไปที่ X ก่อนหน้าtransitionWithView:ลำดับชั้นการดูผลลัพธ์คือ:

  • UIWindow
    • UIView (มุมมองของ X)
    • UIView (มุมมองของ Z)

ถ้าฉันส่งdismissViewControllerAnimated:(ใช่หรือไม่ใช่) ไปที่ X ให้ทำการเปลี่ยนในcompletion:บล็อกจากนั้นลำดับชั้นของมุมมองจะถูกต้อง น่าเสียดายที่รบกวนการเคลื่อนไหว หากทำให้การเลิกจ้างเคลื่อนไหวทำให้เสียเวลา ถ้าไม่เคลื่อนไหวก็ดูเสีย

ฉันกำลังลองวิธีอื่น ๆ (เช่นสร้างคลาสคอนโทรลเลอร์มุมมองคอนเทนเนอร์ใหม่เพื่อใช้เป็นตัวควบคุมมุมมองรูทของฉัน) แต่ไม่พบสิ่งใดที่ใช้ได้ผล ฉันจะอัปเดตคำถามนี้เมื่อไป

เป้าหมายสูงสุดคือการเปลี่ยนจากมุมมองที่นำเสนอไปยังตัวควบคุมมุมมองรูทใหม่โดยตรงและโดยไม่ทิ้งลำดับชั้นมุมมองที่หลงทาง


ฉันมีปัญหาเดียวกันนี้อยู่ในขณะนี้
Alex

ฉันเพิ่งเผชิญกับปัญหาเดียวกัน
Jamal Zafar

โชคดีในการหาทางออกที่ดีสำหรับสิ่งนี้หรือไม่? ปัญหาที่แน่นอนเหมือนกันตรงนี้
David Baez

@DavidBaez ฉันเขียนโค้ดเพื่อปิดตัวควบคุมมุมมองทั้งหมดอย่างจริงจังก่อนที่จะเปลี่ยนรูท แม้ว่าแอปของฉันจะมีความเฉพาะเจาะจงมาก ตั้งแต่โพสต์สิ่งนี้ฉันสงสัยว่าการแลกเปลี่ยนUIWindowเป็นสิ่งที่ต้องทำ แต่ไม่มีเวลาทดลองมากนัก
benzado

คำตอบ:


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
ขอบคุณ. มันได้ผล โปรดแบ่งปันหากคุณพบแนวทางที่ดีกว่า
Jamal Zafar

8
ดูเหมือนว่าการแทนที่ตัวควบคุมมุมมองรูทที่นำเสนอมุมมอง (หรือพยายามยกเลิกการจัดสรร UIWindow ที่ยังคงมีตัวควบคุมมุมมองที่นำเสนออยู่) จะทำให้หน่วยความจำรั่วไหล สำหรับฉันแล้วดูเหมือนว่าการนำเสนอตัวควบคุมมุมมองจะสร้างลูปคงที่กับหน้าต่างและการปิดตัวควบคุมเป็นวิธีเดียวที่ฉันพบว่าจะทำลายมันได้ ฉันคิดว่าบล็อกการสร้างเสร็จภายในบางส่วนมีการอ้างอิงที่ชัดเจนกับหน้าต่าง
Carl Lindberg

มีปัญหากับ NSClassFromString ("UITransitionView") หลังจากแปลงเป็น swift 2.0
Eugene Braginets

ยังคงเกิดขึ้นใน 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. การใช้ TransitionWithView: เพื่อทำให้ Z เป็น Root View Controller ใหม่

ซึ่งให้:

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