Swift Closure Capture Lists
Introduction
In Swift, closures are self-contained blocks of functionality that can be passed around and used in your code. One powerful feature of closures is their ability to capture and store references to variables and constants from the surrounding context in which they are defined. While this is incredibly useful, it can also lead to memory management issues like strong reference cycles.
This is where closure capture lists come in. A capture list allows you to explicitly define how values are captured by the closure, giving you fine-grained control over the memory management behavior of your closures.
Understanding Value Capturing in Closures
Before diving into capture lists, let's understand how Swift closures capture values by default.
func createCounter() -> () -> Int {
var count = 0
let incrementCounter = {
count += 1
return count
}
return incrementCounter
}
let counter = createCounter()
print(counter()) // Output: 1
print(counter()) // Output: 2
print(counter()) // Output: 3
In this example, the incrementCounter
closure captures a reference to the count
variable from its surrounding context. Even after createCounter()
has finished executing, the closure maintains access to count
. This is why the value persists between calls.
The Problem: Reference Cycles
When closures capture references to objects (including self
), and those objects also hold references to the closures, a strong reference cycle can occur:
class PhotoEditor {
var name: String
var onSave: (() -> Void)?
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
func setupSaveAction() {
// This creates a reference cycle!
onSave = {
print("Saving edits made in \(self.name)")
// The closure captures self strongly
}
}
deinit {
print("\(name) is being deinitialized")
}
}
// Create and use a PhotoEditor
do {
let editor = PhotoEditor(name: "Holiday Photos")
editor.setupSaveAction()
editor.onSave?()
} // editor should be deinitialized here, but it won't be due to the reference cycle
If you run this code, you'll notice that the deinitializer never gets called. This is because:
PhotoEditor
instance holds a strong reference to the closure (viaonSave
)- The closure holds a strong reference to the
PhotoEditor
instance (viaself
)
Neither can be deallocated because they're both waiting for the other to release them!
Solving the Problem with Capture Lists
A capture list appears at the start of a closure and defines the rules to use when capturing values. It's enclosed in square brackets []
and placed right before the closure's parameter list.
Weak and Unowned References
Swift provides two ways to create weak references in a capture list:
weak
- Creates an optional weak reference that becomesnil
when the referenced object is deallocatedunowned
- Creates a non-optional reference that's assumed to always have a value (unsafe if the referenced object can be deallocated)
Let's fix our reference cycle:
class PhotoEditor {
var name: String
var onSave: (() -> Void)?
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
func setupSaveAction() {
// Use a capture list with [weak self]
onSave = { [weak self] in
guard let self = self else {
print("PhotoEditor instance has been deallocated")
return
}
print("Saving edits made in \(self.name)")
}
}
deinit {
print("\(name) is being deinitialized")
}
}
// Create and use a PhotoEditor
do {
let editor = PhotoEditor(name: "Holiday Photos")
editor.setupSaveAction()
editor.onSave?()
} // Now editor will be properly deinitialized
// Output: Holiday Photos is being initialized
// Output: Saving edits made in Holiday Photos
// Output: Holiday Photos is being deinitialized
Using [weak self]
creates a weak reference to self
within the closure, breaking the strong reference cycle.
When to Use unowned
Use unowned
when you are sure that the captured reference will never be nil during the lifetime of the closure:
class Tutorial {
var title: String
var onComplete: (() -> Void)?
init(title: String) {
self.title = title
print("Tutorial '\(title)' is being initialized")
}
func setupCompletionAction() {
// Using unowned since this closure will only be called when self exists
onComplete = { [unowned self] in
print("Completed tutorial: \(self.title)")
}
}
deinit {
print("Tutorial '\(title)' is being deinitialized")
}
}
// Example usage
let tutorial = Tutorial(title: "Swift Closures")
tutorial.setupCompletionAction()
tutorial.onComplete?()
Warning: Using unowned
is dangerous if there's any chance the object could be deallocated while the closure still exists. If you access an unowned
reference after the referenced object has been deallocated, you'll get a runtime crash.
Capturing Multiple Values
You can capture multiple values in the capture list, using commas to separate them:
class NetworkManager {
let baseURL: String
var requestCount = 0
init(baseURL: String) {
self.baseURL = baseURL
}
func createDataTask(path: String, completion: @escaping (Data) -> Void) {
let task = { [weak self, localPath = path] in
guard let self = self else { return }
self.requestCount += 1
print("Fetching from \(self.baseURL)/\(localPath)")
// Network request implementation...
}
// Execute task
task()
}
}
let manager = NetworkManager(baseURL: "https://api.example.com")
manager.createDataTask(path: "users") {_ in}
In this example, we capture self
weakly and also create a local copy of the path
parameter.
Value Types in Capture Lists
When you capture value types (like structs or enums) in a closure, they are captured by value, not by reference. This means a copy is made, and you don't need to worry about reference cycles:
struct Counter {
var count = 0
}
func makeIncrementer(counter: Counter) -> () -> Int {
var localCounter = counter // Local copy
return {
localCounter.count += 1
return localCounter.count
}
}
var counter = Counter()
let increment = makeIncrementer(counter: counter)
print(increment()) // Output: 1
print(increment()) // Output: 2
// The original counter is unchanged
print(counter.count) // Output: 0
However, you might still use a capture list to create a snapshot of a value at the time the closure is created:
var temperature = 72.0
let tempLogger = { [capturedTemp = temperature] in
print("The temperature was \(capturedTemp)")
}
temperature = 80.0
tempLogger() // Output: The temperature was 72.0
Best Practices for Using Capture Lists
-
Always use
[weak self]
when creating closures that will be stored in a class instance to avoid reference cycles. -
Use
[unowned self]
only when you're certain the referenced object will outlive the closure - typically in callbacks that are guaranteed to be executed while the object exists. -
Explicitly unwrap weak references for clarity:
someObject.completionHandler = { [weak self] result in
guard let self = self else { return }
// Now use self normally
self.processResult(result)
}
- Consider capturing specific properties instead of the entire
self
object when possible:
class ImageProcessor {
var settings: ProcessingSettings
var onComplete: ((UIImage) -> Void)?
func process(image: UIImage) {
// Capture only the settings, not self
let task = { [settings = self.settings] in
// Process image using settings...
}
}
}
- Document your capture list decisions in complex scenarios to help other developers understand your reasoning.
Real-World Applications
Example 1: Network Request with Cancellation
class NetworkService {
var activeRequests: [URLSessionTask] = []
func fetchData(from url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
defer {
// Remove task from active requests when done
if let index = self?.activeRequests.firstIndex(where: { $0 === task }) {
self?.activeRequests.remove(at: index)
}
}
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(NSError(domain: "NetworkService", code: 1, userInfo: [NSLocalizedDescriptionKey: "No data received"])))
return
}
completion(.success(data))
}
activeRequests.append(task)
task.resume()
}
deinit {
// Cancel all active requests when the service is deallocated
activeRequests.forEach { $0.cancel() }
}
}
Example 2: Animation Completion Handler
class AnimationController {
var isAnimating = false
func animateView(view: UIView, completion: (() -> Void)? = nil) {
isAnimating = true
UIView.animate(withDuration: 0.5, animations: {
view.alpha = 0.0
}, completion: { [weak self] _ in
self?.isAnimating = false
completion?()
})
}
}
Example 3: Timer with Automatic Invalidation
class TimedRefreshController {
private var timer: Timer?
var refreshInterval: TimeInterval
init(refreshInterval: TimeInterval) {
self.refreshInterval = refreshInterval
}
func startRefreshing(onRefresh: @escaping () -> Void) {
// Use capture list to avoid reference cycle with the timer
timer = Timer.scheduledTimer(withTimeInterval: refreshInterval, repeats: true) { [weak self] _ in
guard let self = self else {
// If self is deallocated, invalidate the timer
timer?.invalidate()
return
}
onRefresh()
}
}
func stopRefreshing() {
timer?.invalidate()
timer = nil
}
deinit {
stopRefreshing()
}
}
Summary
Closure capture lists are a powerful feature in Swift that give you precise control over how values are captured in closures. Using them correctly is essential for proper memory management:
- Use
[weak self]
to avoid reference cycles when a closure is stored in a class instance - Use
unowned
only when you're certain the referenced object will outlive the closure - Capture lists can include multiple items, including values you want to copy
- For value types, capture lists can be used to create snapshots of values at the time the closure is defined
By mastering closure capture lists, you'll write more efficient code with fewer memory leaks, leading to more stable and responsive applications.
Exercises
-
Create a class with a method that sets up an asynchronous operation (like a network request) and stores a completion handler as a property. Implement it both with and without a capture list, and observe the difference in memory management.
-
Modify an example where
[weak self]
is used to use[unowned self]
instead. Think about the implications and when this might be appropriate or dangerous. -
Create a closure that captures multiple values in its capture list, including both reference and value types. Experiment with how changes to the original values affect the closure's behavior.
Further Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)