christopherthiebaut.com

Tip: How to avoid unexpected behavior when capturing Optionals

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()
    }
Tagged with: