This being a blog about reactive programming with RxSwift (and friends)–which rely heavily on asynchronicity and closures–I’d like to kick things off by covering one of the most commonly misunderstood things about writing asynchronous code in Swift: how to properly work with reference types in closures.
Fortunately, the rules of engagement are pretty straightforward. Follow them and you should be fine. Ignore them and, well, bad things can happen.
I’ll start with a simple example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import UIKit class ViewController: UIViewController { var random: CGFloat { return CGFloat(drand48()) } override func viewDidLoad() { super.viewDidLoad() srand48(time(nil)) } @IBAction func handleRefreshButtonTapped(_: AnyObject) { let backgroundColor = UIColor(red: random, green: random, blue: random, alpha: 1.0) UIView.animateWithDuration(0.3) { self.view.backgroundColor = backgroundColor } } } |
In handleRefreshButtonTapped(_:)
, I create a random color and then assign it to the view
‘s backgroundColor
in UIView.animateWithDuration(_:animations:)
‘s animations
closure. In the app, each time the refresh button is tapped, the view’s background color will change via a short animation:
By accessing self
in the animations
closure, I am capturing self
(aka closing over self
). Notice that I’ve explicitly written self
in the closure. This is required by the compiler, to make sure I’m aware that self
is being captured in the closure. If I omitted it, I’d get an error:
When a closure captures a reference type, by default it creates a strong reference to that reference type. And if that captured reference type also has a strong reference to the closure, a strong reference cycle is created between the two reference types, and neither can be deallocated. The result is a memory leak.
Capturing self
is harmless in this simple example, because the view controller does not ever get deallocated (it’s a single-view app). But in a more complex app, this could cause a memory leak or even a crash. To demonstrate how that could happen, I’ll modify the example project.
Summary of changes:
- Installed CocoaPods, a fine dependency manager.
- Installed Async, a lightweight wrapper around GCD.
- Installed RxSwift and RxCocoa, the official reactive extensions for Swift and Cocoa.
- Rx-ified the project. More on this in a moment.
- The background-color-changing view controller is now being presented in a popover.
- Added a 5-second delay before changing the background color after tapping the refresh button.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import UIKit import Async import RxSwift import RxCocoa class ViewController: UIViewController { @IBOutlet weak var refreshButton: UIBarButtonItem! var random: CGFloat { return CGFloat(drand48()) } let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() srand48(time(nil)) refreshButton.rx_tap .bindNext { let backgroundColor = UIColor(red: self.random, green: self.random, blue: self.random, alpha: 1.0) Async.main(after: 5.0) { UIView.animateWithDuration(0.3) { self.view.backgroundColor = backgroundColor print("animateWithDuration(_:animations:) was executed") } } } .addDisposableTo(disposeBag) } deinit { print("ViewController will be deallocated") } } |
In viewDidLoad
, I am now binding tap events on the refreshButton
to a closure that does the same thing as was previously being done in handleRefreshButtonTapped(_:)
, except now the animation block is nested in an Async
block that delays its execution by 5 seconds. I’ve also added a couple print
statements so that I can see when the animations
closure is executed, and when ViewController
is about to be deallocated.
Notice that I am capturing self
in the closure parameter to bindNext(_:)
(in addition to the animations
closure, as before).
In the app, I will first tap the refresh button and wait for the animation to complete before tapping Done to dismiss ViewController
, and then I’ll tap the refresh button and Done immediately after, without waiting for the animation to complete. In both cases, ViewController
never gets deallocated, evidenced by the fact that the print
statement in deint
never executes:
The reason ViewController
never gets deallocated is because it has a strong reference to the closure parameter of bindNext(_:)
, and that closure captures and holds a strong reference to the ViewController
. It’s deadlock, and that memory is leaked. If there were enough of these strong reference cycles occurring in an app, the memory pressure would eventually cause the app to be killed.
What I need to do is define a capture list. A capture list defines rules for how to capture one or more reference types in the closure, as follows:
nil
(such as by being deallocated) before closure is executed, define the capture as weak
. If not, that is, the capture and the closure will always be deallocated at the same time, define the capture as unowned
.So now I have to decide whether to define a weak
or unowned
capture of self
.
weak
and unowned
captures will not prevent ARC from disposing of the capture if they are the last reference to that capture. The difference between the two is that weak
can be set to nil
(and, thus, must be an optional), and unowned
cannot.The syntax to define a capture list is to enclose a rule (or multiple rules in a comma-separated list) inside square brackets, within the closure body, after the opening curly brace and before the closure’s parameter list and return value (if provided, and cannot be inferred), followed by the in
keyword. A rule consists of the weak
or unowned
keyword followed by a single capture.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
refreshButton.rx_tap .bindNext { [unowned self] in let backgroundColor = UIColor(red: self.random, green: self.random, blue: self.random, alpha: 1.0) Async.main(after: 5.0) { UIView.animateWithDuration(0.3) { self.view.backgroundColor = backgroundColor print("animateWithDuration(_:animations:) was executed") } } } .addDisposableTo(disposeBag) |
I have modified the bindNext(_:)
call to define an unowned
capture of self
. In the app, if I tap the refresh button and wait for the animation to complete before tapping Done, ViewController
is properly deallocated, the capture of self
is released, and all is good. But, what happens if I do not wait for the animation to complete, and instead tap the refresh button immediately followed by Done? ViewController
is deallocated (self
becomes nil
), but the capture of self
in the closure cannot be set to nil
, because we defined it as unowned
, which is non-optional. The closure will not be released until it is executed. So, when the closure is executed and attempts to access its self
capture, an EXC_BAD_ACCESS
exception is thrown (technically, swift_unknownUnownedTakeStrong()
):
Clearly, unowned
is not the right choice here, because the capture can become nil
before the closure is executed. I must use a weak
capture.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
refreshButton.rx_tap .bindNext { [weak self] in guard let `self` = self else { return } let backgroundColor = UIColor(red: self.random, green: self.random, blue: self.random, alpha: 1.0) Async.main(after: 5.0) { UIView.animateWithDuration(0.3) { self.view.backgroundColor = backgroundColor print("animateWithDuration(_:animations:) was executed") } } } .addDisposableTo(disposeBag) |
I have changed the capture to weak
, which makes it an optional. Then, at the top, I used a guard
statement to unwrap the capture and assign it to a local self
constant if it is not nil
, or else simply return if it is nil
. Thanks to Marin Todorov for sharing the 'self'
idea (source)! That way, I am able to still reference self
within the scope of bindNext(_:)
, but self
is now referring to the local capture. And now all is good:
I hope you enjoyed this article. Nothing says “thanks” like a share. Cheers!