Replacing the UIWindow's rootViewController while using a transition, appears to be leaking
Asked Answered
S

3

11

Environment
iOS 9.2
Xcode 7.2

I'm looking to replace the UIWindow's rootViewController with an animation while also removing it from the view hierarchy as well.

class FooViewController: UIViewController
{
}

class LeakedViewController: UIViewController
{
}

Then initiate the transition in the AppDelegate simply by

    self.window!.rootViewController = LeakedViewController()

    let fooViewController = FooViewController()

    self.window!.rootViewController?.presentViewController(fooViewController, animated: true){ unowned let window = self.window!
        window.rootViewController = fooViewController
    }

Profiling this in Instruments, notice that the rootViewController is still in memory.

enter image description here

Also came across this bug report which seems to suggest the same issue is present in iOS 8.3 and still Open.

Haven't been able to find any references to suggest that as part of the

UIViewController.presentViewController(animated:completion:) 

the source view controller is retained (most likely by the UIPresentationController?) or if this is a bug. Notice that the UIPresentationController was first introduced in iOS 8.

If that's by design, is there an option to release the source view controller?

Using a subclass of UIPresentationController with

override func shouldPresentInFullscreen() -> Bool {
    return true
}

override func shouldRemovePresentersView() -> Bool {
    return true
}

doesn't seem make any difference. Haven't been able to locate anything else in the SDK.

Currently the only way I have found is to use a UIViewController, with a snapshot of what's currently on screen, in place of the root view controller before making the transition.

    let fooViewController = FooViewController()

    let view = self.window!.snapshotViewAfterScreenUpdates(false)
    let viewController = UIViewController()
    viewController.view.addSubview(view)

    self.window!.rootViewController = viewController
    self.window!.rootViewController?.presentViewController(dashboardViewController!, animated: true){ unowned let window = self.window!
        window.rootViewController = fooViewController
    }

It does work, tho in the console the following warning appears

Unbalanced calls to begin/end appearance transitions for <UIViewController: 0x79d991f0>.

Any ideas on the original question or the warning message appreciated.

Update

I believe I have narrowed it down to this one retain that's missing a release.

enter image description here

That is the possible offending call.

 0 UIKit -[UIPresentationController _presentWithAnimationController:interactionController:target:didEndSelector:]
Sporocyst answered 15/1, 2016 at 14:58 Comment(3)
Seeing exactly the same thing, and it's causing me quite the headache... the non released controllers hold onto their old Core Data objects/fetched results controllers and cause crashes. Did you ever come up with a 'nice' solution or anything like that?Raja
@Jordan AFAICS, there is no single solution. Might be as simple as the screenshot I described, throwing out the view hierarchy you don't need (keeping only the leaf) or a way to reuse/release memory depending on your context.Sporocyst
I ended up writing a single file fix - if you're interested I could probably share the code. How it works: starts with an extension for UIViewController that hijacks viewDidLoad() to get a reference to every active view controller. A singleton object uses KVO to observe the window's rootController property, and before any change happens, it sends dismissViewController(animated: false) to every controller in the hierarchy of rootViewController that is about to leak memory.Raja
T
7

I logged that bug report; I have had no response from Apple engineering on it.

The sample code I submitted with the bug report demonstrating the issue is at https://github.com/adurdin/radr21404408

As far as I am aware, the issue is still present in current versions of iOS, but I have not tested exhaustively. Who knows, perhaps 9.3 beta fixes it? :)

In the application where I encountered this bug, we had been using custom transitions and rootViewController replacement for the majority of screen transitions. I have not found a solution to this leak, and because of reasons could not easily remove all the rootViewController manipulation, so instead worked around the issue by minimising where we used presentViewController and friends, and carefully managing the places where we required it.

One approach that I think has potential to avoid the bug while still retaining similar capabilities to rootViewController swapping--but have not yet implemented--is to have the rootViewController be a custom container view controller that occupies the full screen, and defines a presentation context. Instead of swapping the window's rootViewController, I would swap the single child view controller in this container. And because the container defines the presentation context, the presentations will occur from the container instead of the child being swapped. This should then avoid the leaks.

Trichloride answered 15/1, 2016 at 15:39 Comment(1)
Have written a detailed post on How to replace the rootViewController of the UIWindow in iOSSporocyst
A
6

Inspired by @Jordan Smiths comment my fix ended with a one liner (thanks to the beauty of Swift):

window.rootViewController?.dismissViewControllerAnimated(false, completion: nil)

My complete code to swap the rootViewController with an animation then looks like:

func swapRootViewController(newController: UIViewController) {
        if let window = self.window {
            window.rootViewController?.dismissViewControllerAnimated(false, completion: nil)

            UIView.transitionWithView(window, duration: 0.3, options: .TransitionCrossDissolve, animations: {
                window.rootViewController = newController
            }, completion: nil)
        }
    }

With that my memory leak disappeared :-)

Arielariela answered 31/12, 2016 at 8:45 Comment(0)
A
1

The problem is probably that the presented controller and the presenting view controller refer to each other.

I could only get this to work by instantiating two copies of the transitioned-to view controller. One for presenting on the current root and one for replacing the current root after presentation. The copies are easy to achieve for me, since the presented VC's are simple objects. The presented view is left in the window hierarchy after dismissal, so that has to be removed manually after swapping in the new VC.

Here's some Swift.

private func present(_ presented: UIViewController, whenPresentedReplaceBy replaced: @escaping () -> UIViewController)
{
    presented.modalTransitionStyle = .crossDissolve
    let currentRoot = self.window?.rootViewController
    currentRoot?.present(presented, animated: true)
    {
        let nextRoot = replaced()
        self.window?.rootViewController = nextRoot
        currentRoot?.dismiss(animated: false) {
            currentRoot?.view?.removeFromSuperview()
        }
    }
}
Astrionics answered 9/6, 2017 at 13:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.