Search code examples
iosswiftpopoveruistoryboardsegue

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.


Solution

  • 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