We all pretty much know how to safely use self within a Swift closure. But have you ever tried to use self inside a closure that’s inside another closure? There is a big chance that the Swift compiler crashed (Xcode 6.1.1) without giving you an error in the editor and without any error message. So how can you solve this problem?
The basic working closure
Before we dive into the problem and solution, let’s first have a look at a working code sample that only uses a single closure. We can create a simple Swift Playground to run it and validate that it works.
[objc]
class Runner {
var closures: [() -> ()] = []
func doSomethingAsync(completion: () -> ()) {
closures = [completion]
completion()
}
}
class Playground {
let runner = Runner()
func works() {
runner.doSomethingAsync { [weak self] in
self?.printMessage("This works") ?? ()
}
}
func printMessage(message: String) {
println(message)
}
deinit {
println("Deinit")
}
}
struct Tester {
var playground: Playground? = Playground()
}
var tester: Tester? = Tester()
tester?.playground?.works()
tester?.playground = nil
[/objc]
The doSomethingAsync method takes a closure without arguments and has return type Void. This method doesn’t really do anything, but imagine it would load data from a server and then call the completion closure once it’s done loading. It does however create a strong reference to the closure by adding it to the closures array. That means we are only allowed to use a weak reference of self within our closure. Otherwise the Runner would keep a strong reference to the Playground instance and neither would ever be deallocated.
Luckily all is fine and the “This works” message is printed in our playground output. Also a “Deinit” message is printed. The Tester construction is used to make sure that the playground will actually deallocate it.
The failing situation
Let’s make things slightly more complex. When our first async call is finished and calls our completion closure, we want to load something more and therefore need to create another closure within the outer closure. We add the method below to our Playground class. Keep in mind that the first closure doesn’t have [weak self] since we only reference self in the inner closure.
[objc]
func doesntWork() {
weak var weakRunner = runner
runner.doSomethingAsync {
// do some stuff for which we don’t need self
weakRunner?.doSomethingAsync { [weak self] in
self?.printMessage("This doesn’t work") ?? ()
} ?? ()
}
}
[/objc]
Just adding it already makes the compiler crash, without giving us an error in the editor. We don’t even need to run it.
It gives us the following message:
Communication with the playground service was interrupted unexpectedly.
The playground service “com.apple.dt.Xcode.Playground” may have generated a crash log.
And when you have such code in your normal project, the editor also doesn’t give an error, but the build will fail with a Swift Compiler Error without clear message of what’s wrong:
Command /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc failed with exit code 1
The solution
So how can we work around this problem. Quite simple actually. We simply need to move the [weak self] to the most outer closure.
[objc]
func doesWork() {
weak var weakRunner = runner
runner.doSomethingAsync { [weak self] in
weakRunner?.doSomethingAsync {
self?.printMessage("This now works again") ?? ()
} ?? ()
}
}
[/objc]
This does mean that it’s possible that within the outer closure, self is not nil and in the inner closure it is nil. So don’t write code like this:
[objc]
runner.doSomethingAsync { [weak self] in
if self != nil {
self!.printMessage("This is fine, self is not nil")
weakRunner?.doSomethingAsync {
self!.printMessage("This is not good, self could be nil now")
} ?? ()
}
}
[/objc]
There is one more scenario you should be aware of. If you use an if let construction to safely unwrap self, you could create a strong reference again to self. The following sample illustrates this and will create a reference cycle since our runner will create a strong reference to the Playground instance because of the inner closure.
[objc]
runner.doSomethingAsync { [weak self] in
if let this = self {
weakRunner?.doSomethingAsync {
this.printMessage("Captures a strong reference to self")
} ?? ()
}
}
[/objc]
Also this is solved easily by including a weak reference to the instance again, now called this.
[objc]
runner.doSomethingAsync { [weak self] in
if let this = self {
weakRunner?.doSomethingAsync { [weak this] in
this?.printMessage("We’re good again") ?? ()
} ?? ()
}
}
[/objc]
Conclusion
Most people working with Swift know that there are still quite a few bugs in it. In this case, Xcode should give us an error in the editor. If your editor doesn’t complain, but your Swift compiler fails, look for closures like this and correct it. Always be safe and use [weak self] references within closures.