Alexito's World

A world of coding 💻, by Alejandro Martinez

How to capture a reference that doesn't exist yet

Sometimes when writing Swift code you want to reference an instance before its creation. It's not a very common, which is why I'm writing this post so I can easily remember it next time it happens.

To have an example to work with, let's pretend we want to link a custom UIViewController with its ViewModel. This is a typical scenario when building UIKit apps, but the same can apply to any other scenario that involves two classes, inits and closures.

The ingredients for a problematic cycle

One ingredient for this scenario to occur is that you want to inject all dependencies through the initialiser of an object. Yes, there are other ways of injecting dependencies, and if we use any of them this problem would go away. The most common, that you can use in any codebase, is doing injection via a property. But I don't like that at all since it forces your class to deal with optionals (or implicitly unwrapped optionals if you're brave enough) and that just introduces more burden.

The second ingredient you need is a dependency to a closure. This is not typically a problem unless during the creation of the closure you need to reference an uninitialised object.

And that's the third ingredient you need, a cycle. The cycle appears when the object you need in the closure needs the object you're initialising with the closure itself. You can't have one without the other, so you must find a way to break the cycle.

Put in a concrete example:

class ViewController {
    let viewModel: ViewModel
    
    init(viewModel: ViewModel) {
        self.viewModel = viewModel
    }
    
    func run() {
        viewModel.closure() // for example
    }
}

class ViewModel {
    let closure: () -> Void
    
    init(_ closure: @escaping () -> Void) {
        self.closure = closure
    }
}

func create() -> ViewController {
    let viewModel = ViewModel {
        // access viewController here
    }
    let viewController = ViewController(viewModel: viewModel)  
    return viewController
}

As you can see, the ViewController(viewModel: viewModel) needs the viewModel. But let viewModel = ViewModel { ... } needs the viewController. Congratulations, you just cooked a problematic cycle.

Reference the future

The fix for the problem is quite obvious at a first glance. You just need to have a reference to a variable that will be set later.

let viewController: ViewController
let viewModel = ViewModel {
    viewController...
}
viewController = ViewController(viewModel: viewModel)  
return viewController

But that's easier said that done. Because the compiler, caring so much for our safety, won't let us declare a variable and use it before it's initialised. And capturing it in the closure counts as a usage.

Screenshot 2020-10-19 at 17.55.26

To have an uninitialised variable we need to use an Optional, but then you also need to use a var, otherwise you will be forced to assign it before usage.

var viewController: ViewController?
let viewModel = ViewModel {
    viewController?.run()
}
viewController = ViewController(viewModel: viewModel)
return viewController!

Now this works, as in, you can access the instance inside the closure and the compiler is happy about it. But... congratulations! You just created a reference cycle.

Avoiding the reference cycle

We can easily see the reference cycle that is created from the above code:

ViewController

If we focus on the closure, this is not a new issue, Swift developers are used to keep an eye for reference cycles on closures. Typically, we need to be careful with self but in this case we need to be careful with viewController. So let's do the obvious thing and add a capture list that weakifies the instance.

var viewController: ViewController?
let viewModel = ViewModel { [weak viewController] in
    print("access viewController here: \(viewController?.viewModel)")
}
viewController = ViewController(viewModel: viewModel)
return viewController!

This indeed breaks the cycle, but if you run it will see this output:

access viewController here: nil

What is happening here is that the capture list is creating a new weak variable that points to the same memory as the original one. Yes, a new variable. That means that what the closure is capturing is a pointer to nil and that won't change no matter what we do later. So when we're assigning the instance later, we are not changing the variable inside the closure, only the one outside.

To solve this problem, we need to cook the weak variable manually.

Manual weak

Since we can use the capture list, we must reference the original variable. And to avoid the retain cycle, we must have a weak reference. Luckily for us we can declare a local weak variable (something I forgot).

weak var viewController: ViewController?
let viewModel = ViewModel {
    print("access viewController here: \(viewController?.viewModel)")
}
viewController = ViewController(viewModel: viewModel)

We just need to add weak on the declaration and remove the capture list.

But now we have yet another problem. Since the variable is weak, it means nobody is retaining the instance and keeping it in memory. As soon as the method execution ends the viewController will get deallocated.

Screenshot 2020-10-19 at 18.25.35

To solve this, we need to make sure we keep a strong reference to the instance. We need to have two variables, a weak one for the closure and a strong one to retain it. And we need to assign the instance to both of them.

weak var weakViewController: ViewController?
let viewModel = ViewModel {
    print("access viewController here: \(weakViewController?.viewModel)")
}
let viewController = ViewController(viewModel: viewModel)
weakViewController = viewController

And this is the result. We successfully got a reference to an object that still doesn't exist and we made sure there were no retain cycles.

If your code is more complex than this simple snippet, I recommend you to drop an assertion to make sure your future self doesn't forget to assign the instance, without an assert this would silently fail.

Screenshot 2020-10-19 at 18.28.04

Conclusion

Mostly, Swift lets us forget about memory management and references but it's always good to know what's going on under the hood to be able to solve these scenarios when they inevitably occur.

It's also very nice how the compiler has give us errors and warnings on almost every step of this process.

Next time I won't forget I can use weak in local variables ^^