Capturing Self with Swift
Swift blocks are an increasingly common way of providing callback type behavior to asynchronous functions.
Let’s first review what “capturing” means.
Swift blocks look like this:
doSomething(then: {
// do something else
})
Due to the mechanics of memory management, if we need access to local variable within that block, it must be captured.
var dog = Dog()
doSomething(then: {
dog.bark() // dog must be captured so it will live long enough
})
The Swift compiler automatically manages this for you. dog
will be captured (thus increasing its retain count) and you can ensure that even though the dog
instance falls out of the current scope, the block is still retaining it.
Sometimes you don’t want this behavior. The most common reason why is the case where you need to call a method on self
inside the block. The block will retain self
and you’ve just created a bi-directional dependency graph, otherwise known as a retain cycle.
This is bad because it means self
can’t ever be released as long as the block lives. Often times the blocks are stored as properties on the instance, or as observers for KVO or notifications, and their lifetime is bound to self
.
If you’ve done this, congratulations… you have a memory leak.
Avoiding Retain Cycles
To avoid this, you can provide a capture binding declaration to the block definition to influence when a variable should be strongly held or not:
let dog = Dog()
doSomething(then: { [weak dog] in
// dog is now Optional<Dog>
dog?.bark()
)
(So unless we strongly reference dog
in a property or through some other means, it will fall out of scope, and the dog
reference inside the block will be nil.)
This works for self
as well:
doSomething(then: { [weak self] in
self?.doSomethingElse()
)
By declaring a capture as weak
, you’re telling the block to “nil-out” this reference if the original is released.
The Strong-weak dance
If you need to guarantee that the items in the block execute you can create a new strong reference inside the block. For instance the variable became nil while the block was executing, preventing some of the behavior from executing.
doSomething(then: { [weak self] in
guard let strongSelf = self { else return }
strongSelf.doSomethingElse()
)
In the above example, we ensure that our strongSelf
reference exists for the scope of the block.
I don’t care for this naming style, but it conveys exactly what’s going on here. strongSelf
inside the block is a strongly reference variable that will exit scope at the end of the block, thereby releasing it’s reference to self
.
There’s a more concise version of this that I prefer, as of Swift 5.6:
doSomething(then: { [weak self] in
guard let self { else return }
self.doSomethingElse()
)
This syntax works with any optional variable if the unwrapped version has the same name. This avoids the similar guard let self = self
which is the same thing, but is more noise.
Nested Captures
What if you nest blocks?
doSomething(then: { [weak self] in
guard let self { else return }
self.doSomethingElse(then: {
self.finish()
})
)
Here the inner self
refers to our unwrapped version. So what happens is the nested block captures self
from the guard
clause in the first closure. This means you don’t have to do the weak-strong dance within the second closure.
The newer syntax makes this kind of hard to see, but this is equivalent to:
doSomething(then: { [weak self] in
guard let strongSelf = self { else return }
strongSelf.doSomethingElse(then: {
strongSelf.finish()
})
)