Capture Values with Swift Closures
Written by Team Kodeco
Closures in Swift can capture values from the surrounding scope, in order to use them inside the closure body. This allows for a closure to reference variables that are defined outside of its own scope, without the need to pass them as arguments.
Here is an example:
var x = 10
let add = { (a: Int) -> Int in
return a + x
}
print(add(5)) // prints 15
In this example, the closure add
captures the value of x
from the surrounding scope and uses it inside the closure body. The closure takes an argument a
of type Int
and returns the sum of a
and x
. When calling the closure with an argument of 5
, it prints 15
.
Beware of Modifying Captured Variables
It’s important to note that if a captured variable is modified after the closure is defined, the closure will use the updated value. This can lead to unexpected behavior if the closure is called after the variable has been modified.
To avoid this, you can use the let
keyword to define the captured variable as a constant, which will prevent it from being modified. For example:
let x = 10
let add = { (a: Int) -> Int in
return a + x
}
print(add(5)) // prints 15
x = 20 // This will result in a compile-error because x is a constant
In this example, you use the let
keyword to define x
as a constant. This prevents x
from being modified after it’s defined, and if you try to modify it, it’ll result in a compile-error.
This ensures that the closure always uses the value of x
that was captured when it was defined, eliminating any potential unexpected behavior.
Capturing self
Another way to capture values is to use the self
keyword inside the closure to reference the instance of the class or struct that defines the closure.
class SpaceExplorer {
var name: String
var currentPlanet: String
var onPlanetChanged: (() -> ())?
init(name: String, currentPlanet: String) {
self.name = name
self.currentPlanet = currentPlanet
}
func travelToPlanet(planet: String) {
self.currentPlanet = planet
onPlanetChanged?()
}
deinit {
print("Houston, we have a problem...")
}
}
class MissionControl {
let explorer: SpaceExplorer
var planetChangedHandler: (() -> ())?
init(explorer: SpaceExplorer) {
self.explorer = explorer
self.planetChangedHandler = {
print("\(self.explorer.name) has landed on \(self.explorer.currentPlanet)!")
}
self.explorer.onPlanetChanged = planetChangedHandler
}
deinit {
print("Mission terminated.")
}
}
var explorer: SpaceExplorer? = SpaceExplorer(name: "Neil Armstrong", currentPlanet: "Earth")
var missionControl: MissionControl? = MissionControl(explorer: explorer!)
explorer!.travelToPlanet(planet: "Moon")
explorer = nil
missionControl = nil
// Output: Neil Armstrong has landed on Moon!
In this example, you have a class SpaceExplorer
that represents an explorer with a name, current planet and a closure called onPlanetChanged
that gets called when the explorer travels to a new planet. You also have a class MissionControl
that has a reference to an instance of SpaceExplorer
and a closure called planetChangedHandler
.
The MissionControl
class sets the planetChangedHandler
closure as the value for the onPlanetChanged
property of the SpaceExplorer instance. This means that when the travelToPlanet
method is called on the SpaceExplorer
instance, the planetChangedHandler
closure is invoked.
In the planetChangedHandler
closure, you use the self
keyword to reference the instance of the MissionControl
class, and then access the explorer property to access the name
and currentPlanet
properties of the SpaceExplorer
instance.
The problem with this example is that it creates a retain cycle, as the SpaceExplorer
instance holds a strong reference to the planetChangedHandler
closure, which in turn holds a strong reference to the MissionControl
instance. This means that when you set the explorer
and missionControl
variables to nil
, the instances of the SpaceExplorer
and MissionControl
classes aren’t deallocated and their deinit methods aren’t called.
To fix this, use the [weak self]
or [unowned self]
capture list in the closure definition to avoid the retain cycle. For example:
class MissionControl {
let explorer: SpaceExplorer
var planetChangedHandler: (() -> ())?
init(explorer: SpaceExplorer) {
self.explorer = explorer
self.planetChangedHandler = { [weak self] in
print("\(self?.explorer.name) has landed on \(self?.explorer.currentPlanet)!")
}
self.explorer.onPlanetChanged = planetChangedHandler
}
deinit {
print("Mission terminated.")
}
}
This way the explorer and missionControl
instances will be deallocated correctly, and their deinit
methods will be called.
It’s important to note that when using the [weak self]
capture list, the self
reference inside the closure is now an optional.
This means that you must unwrap the optional first, such as by using the optional chaining operator ?
as in this example, or by using another method such as an if-let
statement.