Capturing objects in Swift closures
Basics article available: ClosuresEver since blocks were introduced into Objective-C as part of iOS 4 they have been an important part of most modern APIs for Apple's platforms. The convention of using blocks also carried over to Swift with closures, which is a language feature that most of us use every single day.
But even though closures are very widely used, there's a lot of behaviors and caveats to keep in mind when using them. This week, let's take a closer look at closures, how capturing works and some techniques that can make handling them easier. Let's dive in!
The great escape
Closures come in two different variants - escaping and non-escaping. When a closure is escaping (as marked by the @escaping
parameter attribute) it means that it will be stored somehow (either as a property, or by being captured by another closure). Non-escaping closures on the other hand, cannot be stored and must instead be executed directly when used.
An example of non-escaping closures is when using functional operations on a collection, for example forEach
:
[1, 2, 3].forEach { number in
...
}
Since the closure will be executed directly for each member of the collection, there's no need for it to be escaping.
Escaping closures are mostly found in asynchronous APIs, such as DispatchQueue
. For example, when you are scheduling an asynchronous closure, that closure will escape:
DispatchQueue.main.async {
...
}
So, what's the difference? Since escaping closures will be stored, they also need to store the context in which they were defined. When that context involves other values or objects, those need to be captured, as to not disappear while the closure is pending execution. The most common practical implication of this is when using APIs on self
, which requires us to explicitly capture self
one way or another.
Capturing & retain cycles
Since escaping closures automatically capture any value or object that is being used within them, they're a quite common source of retain cycles. For example, when a view controller will be captured in a closure stored by its view model:
class ListViewController: UITableViewController {
private let viewModel: ListViewModel
init(viewModel: ListViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
viewModel.observeNumberOfItemsChanged {
// This will cause a retain cycle, since our view controller
// retains its view model, which in turn retains the view
// controller by capturing it in an escaping closure.
self.tableView.reloadData()
}
}
}
A common fix to this problem, as most of you who have worked with closures already probably know, is to capture self
weakly to break the retain cycle:
viewModel.observeNumberOfItemsChanged { [weak self] in
self?.tableView.reloadData()
}
Capturing the context instead of self
While the above [weak self]
solution is great for most situations when you want to avoid capturing an object strongly, it also has some downsides. First of all, it's super easy to miss as the compiler won't warn you of any potential retain cycles. Secondly, it can lead to some pretty messy code when you have to convert back from a weak reference to a strong one, like this:
dataLoader.loadData(from: url) { [weak self] data in
guard let strongSelf = self else {
return
}
let model = try strongSelf.parser.parse(data, using: strongSelf.schema)
strongSelf.titleLabel.text = model.title
strongSelf.textLabel.text = model.text
}
One alternative solution to capturing self
is to instead capture the individual objects that you need inside of the closure. This still lets us avoid a retain cycle (since the objects like our labels and schema don't store the closure), without having to do the "weak/strong self dance". Here's how that can be done using a context
tuple:
// We define a context tuple that contains all of our closure's dependencies
let context = (
parser: parser,
schema: schema,
titleLabel: titleLabel,
textLabel: textLabel
)
dataLoader.loadData(from: url) { data in
// We can now use the context instead of having to capture 'self'
let model = try context.parser.parse(data, using: context.schema)
context.titleLabel.text = model.title
context.textLabel.text = model.text
}
Arguments instead of capturing
Another alternative to capturing objects is to pass them as arguments. This is a technique that I used when designing the event API for my new game engine Imagine Engine, which lets you pass an observer when observing an event using a closure. This enables self
to be passed in, which in turn will be passed into the closure for the event, without having to capture it manually:
actor.events.moved.addObserver(self) { scene in
...
}
Let's go back to our initial ListViewController
example, and have a look at how we can accomplish the exact same API for when observing its view model. That way we could even pass in the table view that we want to reload as an observer, giving us a very nice call site, like this:
viewModel.numberOfItemsChanged.addObserver(tableView) { tableView in
tableView.reloadData()
}
To make the above happen, we'll use a technique very similar to how Imagine Engine's event system works. We'll start by defining a simple Event
type that can have observation closures attached to it:
class Event {
private var observers = [() -> Void]()
}
Then, we'll add a method that lets us add an observer of any reference type, along with a closure to call once the observation is triggered. Here comes the trick, we'll wrap the closure that was passed into a second one, that captures the observer weakly under the hood, like this:
func addObserver<T: AnyObject>(_ observer: T, using closure: @escaping (T) -> Void) {
observers.append { [weak observer] in
observer.map(closure)
}
}
This enables us to only have to do the weak/strong conversion once, without affecting the call site. Finally, we'll add a trigger
method that lets us trigger the event:
func trigger() {
for observer in observers {
observer()
}
}
We can now go back to our ListViewModel
and add an event for numberOfItemsChanged
that we'll trigger once its condition is met, like this:
class ListViewModel {
let numberOfItemsChanged = Event()
var items: [Item] { didSet { itemsDidChange(from: oldValue) } }
private func itemsDidChange(from previousItems: [Item]) {
if previousItems.count != items.count {
numberOfItemsChanged.trigger()
}
}
}
The big advantage of an event-based API like the above, is that it gets much harder to accidentally introduce retain cycles, and we can reuse the same implementation for any type of event observations in our code. While the above Event
implementation is very simple and lacks advanced features like being able to unregister an observer, it's good enough for simpler use cases.
We'll take a much closer look at event-based programming in general in future blog posts, and you can also check out the full Event implementation used in Imagine Engine as well for more details.
Conclusion
That closures automatically capture any object or value that are used inside them is an awesome feature, and essential to making them easy to work with. However, capturing can also be a source of bugs & retain cycles, and can make code more complex and harder to understand.
While I don't recommend avoiding capturing in all cases, I hope that this post has presented some alternatives to always capturing self
specifically. In some situations, doing the classic weak self
capture is the most appropriate solution, but for others using some alternative techniques can help you make your closure-based code a lot easier to use & maintain.
What do you think? Do you have any other tips when it comes to capturing objects in closures? Let me know, along with any other comments, feedback or questions you might have - on Twitter @johnsundell.
Thanks for reading! 🚀