Tip: How to avoid unexpected behavior when capturing Optional
s
Swift's Optional
type is one of the defining features of the language. Given the amount of language features and syntactic sugar that are built around Optional
, it is easy to forget that there even is such a type.
Normally, it's a good thing for a language feature to get out of your way. But when it comes to closure capture lists, it's important to remember that an optional is a value type regardless of what the wrapped type is.
Consider the following example code:
class ExampleViewController: UIViewController {
weak var delegate: ExampleViewControllerDelegate?
}
It is easy to look at the above code and think of delegate
as being an ExampleViewControllerDelegate
which may or may not be present. But that thinking will lead you astray if you try to need to use an optional in a closure capture list. Expanding on the previous example (assuming it was executed in a Playground):
class ExampleViewControllerDelegate {
var delegateMethodCalled = false
func delegateMethod() {
delegateMethodCalled = true
}
}
class ExampleViewController : UIViewController {
weak var delegate: ExampleViewControllerDelegate?
override func viewDidLoad() {
viewDidLoad()
URLSession.shared.dataTask(with: exampleURL) { [delegate] _, _, _ in
delegate?.delegateMethod()
}.resume()
}
}
let vc = ExampleViewController()
_ = vc.view
let delegate = ExampleViewControllerDelegate()
vc.delegate = delegate
print(delegate.delegateMethodCalled)
What do we suppose will happen when the code above executes? Surely the delegate will be set before the URLSession
callback is called. So intuitively, we'd expect it to print true
. But that's because we're thinking about delegate
as a reference type when it's actually a value type.
When the closure is created, delegate
is nil
so we actually capture the enum case Optional<ExampleViewControllerDelegate>.none
. Since enums are value types, this will not get updated when the view controller's delegate gets set.
The simplest way to avoid bugs like that would be to use an actual reference type in your capture lists if you're expecting the closure to have the latest value when it is called. In the case of your example, that would be:
override func viewDidLoad() {
viewDidLoad()
URLSession.shared.dataTask(with: exampleURL) { [weak self] _, _, _ in
self?.delegate?.delegateMethod()
}.resume()
}