Swift’s closure capturing mechanics
Basics article available: ClosuresClosures are an increasingly important part of Swift, both in terms of the overall direction of the language itself, and when it comes to the ways that both Apple and third party developers design libraries and APIs using it. However, closures also come with a certain set of complexities and behaviors that at first can be quite difficult to fully grasp — especially when it comes to how they capture values and objects from their surrounding context in order to perform their work.
While we already took a look at various ways to capture objects within closures in 2017’s “Capturing objects in Swift closures”, this week’s, let’s explore the concept of capturing more broadly — by taking a closer look at some of the opportunities and challenges that comes with writing capturing closures in general.
Implicit capturing
Whenever we’re defining an escaping closure — that is, a closure that either gets stored in a property, or captured by another escaping closure — it’ll implicitly capture any objects, values and functions that are referenced within it. Since such closures may be executed at a later time, they need to maintain strong references to all of their dependencies in order to prevent them from being deallocated in the meantime.
For example, here we’re using Grand Central Dispatch to delay the presentation of a UIAlertController
by three seconds, which requires the closure passed into the call to asyncAfter
to capture the presenter
view controller instance:
func presentDelayedConfirmation(in presenter: UIViewController) {
let queue = DispatchQueue.main
queue.asyncAfter(deadline: .now() + 3) {
let alert = UIAlertController(
title: "...",
message: "...",
preferredStyle: .alert
)
// By simply refering to 'presenter' here, our closure
// will automatically capture that instance, and retain
// it until the closure itself gets released from memory:
presenter.present(alert, animated: true)
}
}
While the above behavior is really convenient, it can also become the source of some really tricky bugs and memory-related issues if we’re not careful.
For example, since we’re delaying the execution of the above code by a few seconds, it’s possible for our presenter
view controller to have been removed from our app’s view hierarchy by the time the closure actually gets run — and while that wouldn’t be a catastrophe in this case, it would arguably be better for us to only present our confirmation if the view controller is still being retained by another object (presumably its parent view controller or window).
Using capture lists
This is where capture lists come in, which enable us to customize how a given closure captures any of the objects or values that it refers to. Using a capture list, we can instruct our above closure to capture the presenter
view controller weakly, rather than strongly (which is the default). That way, the view controller will get deallocated if not referenced by any other part of our code base — resulting in memory getting freed up quicker, and no unnecessary operations being performed:
func presentDelayedConfirmation(in presenter: UIViewController) {
let queue = DispatchQueue.main
// A capture list is defined using a set of square brackets
// directly following a closure's opening curly bracket:
queue.asyncAfter(deadline: .now() + 3) { [weak presenter] in
// Here we verify that our presenter is still in memory,
// otherwise we can return early:
guard let presenter = presenter else { return }
let alert = UIAlertController(
title: "...",
message: "...",
preferredStyle: .alert
)
presenter.present(alert, animated: true)
}
}
Capture lists are perhaps even more useful when we need to reference self
, especially when doing so would cause a retain cycle, which is when two objects or closures refer to each other — preventing both of them from ever getting deallocated (since they can’t reach a reference count of zero).
Here’s an example of such a situation, in which we’re using a capture list to avoid referencing self
strongly within a closure that will also be retained by self
:
class UserModelController {
let storage: UserStorage
private var user: User { didSet { userDidChange() } }
init(user: User, storage: UserStorage) {
self.storage = storage
self.user = user
storage.addObserver(forID: user.id) { [weak self] user in
self?.user = user
}
}
}
Alternatively, we could’ve converted the above user
property into a function using its key path, like we did in “The power of key paths in Swift” — since all that we’re doing inside of our observation closure is updating that property’s value.
The reason the above closure would end up causing a retain cycle if we didn’t capture self
weakly is because UserStorage
will retain that closure, and self
already retains that object through its storage
property.
Weak references are not always the answer
While the above two code samples might make it seem like always capturing self
weakly is the way to go, that’s definitely not the case. Like with other kinds of memory management, we’ll have to carefully consider how self
will be used within each situation, and for how long we expect each capturing closure to remain in memory.
For example, if we’re dealing with really short-lived closures, such as ones passed to the UIView.animate
API (which are just executed to perform interpolation for an animation, and then released), capturing self
is really not a problem, and will most likely lead to code that’s easier to read:
extension ProductViewController {
func expandImageView() {
UIView.animate(withDuration: 0.3) {
self.imageView.frame = self.view.bounds
self.showImageCloseButton()
}
}
}
Note how we always need to explicitly refer to self
when accessing both instance methods and properties within an escaping closure. That’s a good thing, as it requires us to make an explicit decision to capture self
, given the consequences that doing so might have.
There are also many kinds of situations in which we might want to retain self
even for longer — for example if the current object is required in order to perform a closure’s work, like in this case:
extension NetworkingController {
func makeImageUploadingTask(for image: Image) -> Task {
Task { handler in
let request = Request(
endpoint: .imageUpload,
payload: image
)
// The current NetworkingController is required here,
// so for as long the returned task is retained,
// we'll also retain its underlying controller:
self.perform(request, then: handler)
}
}
}
The above code won’t cause any retain cycles, since NetworkingController
doesn’t retain the tasks that it creates. For more sophisticated ways of modeling and working with tasks in Swift, check out “Task-based concurrency in Swift”.
We can also capture each of a closure’s dependencies directly, rather than referencing self
— again using a capture list. For example, here we’re capturing an image loader’s cache
property in order to be able to use it once an image was successfully downloaded:
class ImageLoader {
private let cache = Cache<URL, Image>()
func loadImage(
from url: URL,
then handler: @escaping (Result<Image, Error>) -> Void
) {
// Here we capture our image loader's cache without
// capturing 'self', and without having to deal with
// any optionals or weak references:
request(url) { [cache] result in
do {
let image = try result.decodedAsImage()
cache.insert(image, forKey: url)
handler(.success(image))
} catch {
handler(.failure(error))
}
}
}
}
The above technique works really well when we only need access to a few of our properties, rather than to self
as a whole — as long as those properties either contain reference types (class instances), or immutable value types.
Capturing values
Value types can sometimes be a bit more complex to deal with when it comes to closure capturing, since they’re passed to external scopes as copies, rather than as references. Although that’s exactly what makes Swift’s value types so powerful, it can have somewhat unexpected consequences in situations like the one below — in which we’re capturing a sender
and message
property when assigning a handler
closure to a button:
class MessageComposerViewController: UIViewController {
private let sender: MessageSender
private var message = Message()
private lazy var sendButton = ActionButton()
...
override func viewDidLoad() {
super.viewDidLoad()
...
sendButton.handler = { [sender, message] in
sender.send(message)
}
}
}
At first glance, the above code may seem perfectly fine. However, the Message
type that we’re using above is implemented as a struct, which gives it value semantics — meaning that we’re just capturing its current value when adding it to our capture list. So even though that value might change during our view controller’s lifecycle, once our sendButton
is tapped, we’ll still send our original value — which isn’t great.
One way to solve the above problem, while still avoiding any additional guard
statements, would be to only capture self
to be able to access its message
— and then map that value directly to our sender’s send
method, like this:
sendButton.handler = { [weak self, sender] in
let message = self?.message
message.map(sender.send)
}
However, the above is really only a problem when dealing with mutable values. If we instead only have constants, like in the following example, then we can add those properties to any closure’s capture list without any problems (as their values won’t change):
class ProductViewController: UIViewController {
private let productManager: ProductManager
private let product: Product
...
override func viewDidLoad() {
super.viewDidLoad()
...
buyButton.handler = { [productManager, product] in
productManager.startCheckout(for: product)
}
}
}
In situations like the one above, we could also create a new function by combining our value (product
in this case) with the method that it’ll be passed into. Check out the first episode of Swift Clips for an example of doing just that.
Finally, let’s take a look at how values are captured when it comes to local variables. Contrary to how value-based properties are captured, local variables still maintain their connection to their original declaration when captured by a closure within the same scope — which can be incredibly useful in order to keep track of various kinds of state.
For example, let’s say that we wanted to extend Swift’s Collection
protocol with an API to enable us to iterate over any collection using a buffer consisting of the current element, as well as the next one. That could be done by combining the standard library’s AnySequence
and AnyIterator
types with locally captured values — like this:
extension Collection {
typealias Buffer = (current: Element, next: Element?)
var buffered: AnySequence<Buffer> {
AnySequence { () -> AnyIterator<Buffer> in
// We define our state as local variables:
var iterator = self.makeIterator()
var next: Element?
return AnyIterator { () -> Buffer? in
// We can then use our state to make decisions by
// capturing them within our iterator's closure:
guard let current = next ?? iterator.next() else {
return nil
}
next = iterator.next()
return (current, next)
}
}
}
}
For more information about the above way of creating custom sequences, check out “Wrapping sequences in Swift”.
So values copied when captured using a capture list, while they’re not copied when referenced directly — for example when accessed as properties, or when a local variable is captured within the same scope as it was defined in.
Unowned references
One final option when it comes to closure capturing is to use unowned
references. These are, just like weak
references, specified using capture lists — and can also only be applied to reference types. Using unowned
gives us essentially the same result as when using force-unwrapped optionals, in that it lets us treat a weak reference as if it was non-optional, but will result in a crash if we try to access it after it was deallocated.
Going back to our UserModelController
example from before, here’s what it’d look like if we were to use unowned
instead of weak
:
class UserModelController {
...
init(user: User, storage: UserStorage) {
...
storage.addObserver(forID: user.id) { [unowned self] user in
self.user = user
}
}
}
While using unowned
lets us get rid of optionals, and might occasionally be really convenient, the fact that it causes crashes for deallocated references makes it quite dangerous to use unless we’re absolutely certain that a given closure won’t accidentally be triggered after one of its dependencies has been deallocated.
One advantage of such crashes, however, is that they let us identify code paths that ideally should never have been entered. For example, if our above observation closure ends up getting triggered after self
was deallocated, that probably means that we’re not unregistering our observations properly, which would be great to know.
However, rather than using unowned
, we could (in this case) achieve the exact same thing using an assert
— and while doing so will result in a bit more code, it’ll also give us a much more actionable error message in case of a failure, and we wouldn’t be causing any crashes in production:
class UserModelController {
...
init(user: User, storage: UserStorage) {
...
storage.addObserver(forID: user.id) { [weak self] user in
assert(self != nil, """
It seems like UserModelController didn't unregister \
itself as a storage observer before being deallocated
""")
self?.user = user
}
}
}
To learn more about assert
, along with other ways of propagating various errors — check out “Picking the right way of failing in Swift”.
Conclusion
Although Swift’s Automatic Reference Counting memory management model doesn’t require us to manually allocate and deallocate memory, it still requires us to decide exactly how we want our various objects and values to be referenced.
While it’s common to hear over-simplified rules like “Always use weak
references within closures”, writing well-performing and predictable apps and systems often requires a bit more nuanced thinking than that. Like with most things within the world of software development, the best approach tends to be to throughly learn the underlying mechanics and behaviors, and then choose how to apply them within each given situation.
Hopefully this article has provided a few insights into those mechanics and behaviors when it comes to closure capturing, and if you have any questions, comments or feedback — just let me know, either on Twitter or via email.
Thanks for reading! 🚀