Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using adaptive popover segue and wrapping the destination in a navigation controller leads to memory leaks

Let's say I have a view controller that I show using an adaptive popover segue when clicking on a button. Now in some cases, I might want to wrap the destination view controller in (for example) a navigation controller. So, I set myself as the delegate for the popoverPresentationController's delegate, and implement the presentationController:viewControllerForAdaptivePresentationStyle: method.


But I noticed something strange: in some cases, objects were not being deallocated. If, in the previously mentioned method, I wrap the presented viewcontroller in a navigation controller:

func presentationController(controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController? {
    return UINavigationController(rootViewController: controller.presentedViewController)
}

On dismiss the navigation controller gets deallocated, but the presented view controller remains allocated.

If, in contrast, I directly show a navigation controller via adaptive popover segue, then on dismiss both the navigation controller and the details controller it contains get deallocated correctly.


For demonstration purposes, please refer to this test project (Swift): https://github.com/djbe/AdaptivePopoverSegue-Test

What we get when dynamically wrapping in a navigation controller (tap the "Popover, nav automatically added" button):

--- Showing details ---
Loaded details view controller (0x7fab31632b70)
Loaded navigation controller (0x7fab32815600)
Deinit navigation controller (0x7fab32815600)

As you can see, the details view controller is never deallocated.


I checked the documentation for presentationController:viewControllerForAdaptivePresentationStyle: but there are no specific mentions of ownership, strong retains, etc... I tried using Instruments with the Allocations tool, but there are so many retain/releases involved in this (simple) case that I couldn't directly find the problem.

Has anyone ever encountered this issue? Or do you have an idea on how to solve this?


Solution

As mentioned below by @TomSwift, there is a bug due to a circular reference between the controller and the segue. The only way to solve this, and still wrap the destination controller in a navigation controller, is by doing the wrapping in the init method of the segue (custom).

I've updated my sample code on Github to showcase how this would be achieved using the solution as mentioned by @Vasily, but still allow for dynamic wrapping behaviour using protocols, without resorting to hacky workarounds using NSUserDefaults.

like image 654
djbe Avatar asked Aug 17 '16 18:08

djbe


2 Answers

Using XCode8 I noted that there is a circular reference between the DetailsViewController and the UIStoryboardSegue. I don't see a way to cleanly break this cycle as it's internal to UIKit. There's seemingly a secondary circular reference involving an NSDictionary ivar "_externalObjectsTableForLoading". You should report this to Apple!

enter image description here

A solution is to not reuse the DetailsViewController that was pre-loaded by the segue. If you manually instantiate it yourself you can bypass this problem. Here's a possible implementation (requires you set the restoration identifier in the storyboard!):

func presentationController(controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController? {
    if (wrapInNavigationController) {
        let vc = controller.presentedViewController
        if let restorationIdentifier = vc.restorationIdentifier {
            return NavigationController(rootViewController: vc.storyboard!.instantiateViewControllerWithIdentifier(restorationIdentifier))
        }
    }
    return controller.presentedViewController
}
like image 190
TomSwift Avatar answered Nov 07 '22 17:11

TomSwift


Solution

You need to create custom UIStoryboardSegue class and override init function.

Sample:

class StoryboardSegue: UIStoryboardSegue {

override init(identifier: String?, source: UIViewController, destination: UIViewController) {
    super.init(identifier: identifier, source: source, destination: NavigationController(rootViewController: destination))
}
}

Main.storyboard

enter image description here

result

enter image description here

like image 22
Vasily Bodnarchuk Avatar answered Nov 07 '22 15:11

Vasily Bodnarchuk