Resolving views when changing rootViewController inside transitionWithView

When investigating a memory leak, I found a problem with the method of calling setRootViewController: inside the transition animation block:

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

If the old view controller (the one that is being replaced) is currently a different view controller, then the code above does not remove the presented view from the view hierarchy.

That is, this sequence of operations ...

  • X becomes Root View Controller
  • X represents Y, so Y is the view on the screen
  • Using transitionWithView: to make Z the new root view controller

... looks good to the user, but the Debug View Hierarchy tool will show that the Y image is still behind the Z view, inside the UITransitionView . That is, after three steps above, the presentation hierarchy:

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

I suspect this is a problem because during the transition, view X is not actually part of the view hierarchy.

If I send dismissViewControllerAnimated:NO to X just before transitionWithView: resulting view hierarchy:

  • UIWindow
    • UIView (view X)
    • UIView (view Z)

If I send dismissViewControllerAnimated: (YES or NO) to X, go through the completion: block, then the view hierarchy is correct. Unfortunately, this interferes with the animation. If you revive the dismissal, he spends time; if he does not revive, he looks broken.

I am trying to use other approaches (for example, creating a new view controller class as my root view controller), but have not found anything. I will update this question when I go.

The ultimate goal is to move from the presented view to the new root view controller directly and without leaving a hierarchy of inaction.

+91
ios cocoa-touch uiviewcontroller core-animation
Nov 05 '14 at 17:08
source share
4 answers

I recently had a similar problem. I had to manually remove this UITransitionView from the window in order to fix the problem, and then call dismiss on the previous root view controller to make sure it was released.

The fix is ​​not very good, but if you have not found a better way from the moment you posted the question, this is the only thing I found for work! viewController is just the newController from your original question.

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

Hope this also helps you solve the problem, this is an absolute pain in the ass!

Swift 3.0

(See change history for other versions of Swift)

For a more pleasant implementation, as an extension in UIWindow , allowing you to pass an optional transition.

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

Using:

 window.set(rootViewController: viewController) 

Or

 let transition = CATransition() transition.type = kCATransitionFade window.set(rootViewController: viewController, withTransition: transition) 
+113
Nov 26 '14 at 16:16
source share

I ran into this problem and it annoyed me all day. I tried the solution @Rich obj-c and it turns out that after that I want to introduce another viewController, I will be blocked by an empty UITransitionView.

Finally, I figured it out, and it worked for me.

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

Ok, now you only need to call [self setRootViewController:newViewController]; if you want to switch the root view controller.

+5
Jan 07 '16 at 3:38 on
source share

I am trying a simple thing that works for me on iOS 9.3: just remove the old viewController view from your hierarchy during dismissViewControllerAnimated completion.

Let me work on the X, Y, and Z views as described in benzado :

That is, this sequence of operations ...

  • X becomes Root View Controller
  • X represents Y, so Y is the view on the screen
  • Using the transition ViewView: make Z the new Root View Controller

What gives:

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

In my case, X and Y are well dealloc, and their representation is no longer in the hierarchy!

+5
Aug 02 '16 at 13:32
source share

I came up with this problem when using this code:

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

Disabling this code fixes the problem. I managed to get this working just by turning on this transitional animation when the filter panel that gets animated is initialized.

This is not the answer you are looking for, but it may lead you to the right panel for finding your solution.

-2
Jan 19 '15 at 13:37
source share



All Articles