Chapters

Hide chapters

Swift Cookbook

Live Edition · Multiplatform · Swift · Editor agnostic

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.

© 2024 Kodeco Inc.