3

Let me start this question off by stating that there are no upright desks left in my office. I've turned them all over. I need help understanding what is happening in Apple's UINavigationController that is causing my weak reference to zero out.


First, before I explain the problem in any more detail, let me provide the code to reproduce.

First, create a subclass of UINavigationController. This subclass will hold a weak reference to a UIViewController subclass. It should be a reference to what will be passed in as the root view controller.

For evidence of my insanity, I've provided an initializer that reproduces the problem as well as an initializer that fixes my problem. For completeness, I've included the other initializers you need to make this compile.

class ChildNavController: UINavigationController {

    weak var weakRoot: UIViewController?

    init(withWeakFirst: Void) {
        let root = UIViewController()
        print("rootVC: \(root)")
        weakRoot = root
        super.init(rootViewController: root)

    }

    init(withWeakLast: Void) {
        let root = UIViewController()
        print("rootVC: \(root)")
        super.init(rootViewController: root)
        weakRoot = root
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }

}

The print statements are strictly unnecessary, but I've included them because the default description of a UIViewController includes the memory address.

Now... the code to test what is happening, which is fairly straight-forward...

First, testing my first initializer:

let weakFirst = ChildNavController(withWeakFirst: ())
if let _ = weakFirst.weakRoot {
    print("weak first non-nil")
}
else {
    print("weak first nil")
    print("but the rootVC? \(weakFirst.viewControllers.first)")
}

The console output for this is:

rootVC: <UIViewController: 0x7f9de9f091a0>
weak first nil
but the rootVC? Optional(<UIViewController: 0x7f9de9f091a0>)

And comparing that to the second initializer, where I simply change the order between calling super.init and assigning into my weak property:

let strongFirst = ChildNavController(withWeakLast: ())
if let _ = strongFirst.weakRoot {
    print("weak last non-nil")
}
else {
    print("weak last nil")
    print("but the rootVC? \(weakFirst.viewControllers.first)")
}

And the console output for this:

rootVC: <UIViewController: 0x7f9de9d11f70>
weak last non-nil

There are only three relevant lines of code here. The first line initializes a regular UIViewController:

let root = UIViewController()

This is assigned into a local variable. The local variable goes away when the initializer returns, so if no external strong reference is created, the view controller should be deallocated.

Then I have two more lines.

weakRoot = root

and

super.init(rootViewController: root)

Somehow, the order of these two lines determines whether my weakRoot property is nil or is a reference to the same object contained in the 0th index of super.viewControllers.

This does not make sense to me and I need help understanding what is going on.


Addendum: I have tried reproducing this problem with my own super classes. I've tried with Swift super classes and with Objective-C super classes. I can not reproduce the problem in those cases. I am only able to reproduce with UINavigationController (however I do not know if there might be other Apple framework classes that replicate this behavior).

nhgrif
  • 58,130
  • 23
  • 123
  • 163
  • Are you not looking at asynchronous code execution by any chance? The super.init takes more time which might make it look like failure. Can you test with a few extra println and maybe a wait – Norbert van Nobelen Aug 14 '18 at 20:35
  • I don't see how anything asynchronous happening makes sense. My weak reference should only zero out in the case that the object it refers to is deallocated. Given that the navigation controller still has a reference to the object that the weak property should have a reference to, it seems something else is clearly happening. – nhgrif Aug 14 '18 at 20:42
  • I also don't believe there is anything asynchronous happening in this code. At all. I doubt it is. I'm not sure where I'd put any more prints or put a wait, or how any of that would change anything. – nhgrif Aug 14 '18 at 20:43
  • For the print: Add a print to every init for weakRoot as last line of the init. That can pretty much only show that weakRoot is initialized and can give insight in async execution – Norbert van Nobelen Aug 14 '18 at 20:48
  • What part of this do you believe is happening asynchronously? – nhgrif Aug 14 '18 at 21:14
  • For what it's worth, I believe this is likely related to specific implementation details with regards to Swift initialization, which I explain in detail [here](https://stackoverflow.com/a/32108404/2792531), but I believe this specific behavior is probably a bug. What I don't understand is what it is about `UINavigationController` in specific that causes the problem. – nhgrif Aug 14 '18 at 21:18
  • [Here](https://i.stack.imgur.com/ehkft.png) is a screenshot with some logging added. The weak reference is being zero'd out too early (before the strong reference from the local `root` variable goes away). – nhgrif Aug 14 '18 at 21:21
  • It is not just for a `weak` variable. Any modification to any variable before `super` is called is nullified. _thinking_ – Rakesha Shastri Aug 14 '18 at 21:55
  • @RakeshaShastri this is completely inaccurate. I recommend taking a read through [this answer](https://stackoverflow.com/a/32108404/2792531) which explains a lot about how Swift initializers work. Waiting till after super to assign properties simply wouldn't compile in a lot of scenarios...but more importantly, if I change the super class away from `UINavigationController` and to my own customer super class, my weak variable is not nullified no matter the order of execution. – nhgrif Aug 15 '18 at 02:10
  • I was talking in context with reference to your example. I put that code in the playground, first ran it with a strong reference of root - _it got nullified_, then again made another integer variable with a default value 0 and set it to 1 before super was called - _the variable was reset to 0_ – Rakesha Shastri Aug 15 '18 at 04:58
  • Hmm. I see that. When I remove the `weak` keyword, I get the same results, so perhaps this problem doesn't have to do with `weak`. However, the fact remains that when I make a custom super class, this behavior does not repeat. Calling `super.init` emphatically does *not* nullify these properties as a rule in Swift. In fact, if the property is defined as a `let` property rather than a `var`, I am **required** to set it *before* calling `super.init` (otherwise it doesn't compile) and trying to change it after `super.init` is a compiler error (because it's a constant). – nhgrif Aug 15 '18 at 13:23

0 Answers0