· 6 min read Posted by Ben Whitley
Swift Closures in Kotlin Multiplatform
When working on the iOS side of Kotlin Multiplatform (KMP) projects, you’ll probably find yourself using Swift closures to update your UI. At Touchlab, we find doing so to be best practice for most use cases. However, if you’re not familiar with closures in Swift, they can pose a risk of creating strong reference cycles.
Opening the ARC
Before diving into an explanation of strong reference cycles and how to avoid them, first some broader context on the subject. The first thing to note is that Automatic Reference Counting (ARC) is what manages memory in Swift and Objective-C. For each object instance, it keeps count of the number of references to that instance, incrementing and decrementing it when references are created or destroyed, and ultimately reclaiming the memory when that count is zero.
ARC is a huge topic all on its own, but know that it handles almost all of the work for you, almost all of the time, so that you don’t have to worry about it. However, ARC isn’t perfect, and we’re here to go over one of the times that you do need to worry about it.
Getting Closures
When writing Swift code that’s calling a shared Kotlin library, closures are probably where developers are most likely to accidentally create a strong reference cycle. According to the official Swift documentation, “Closures can capture and store references to any constants and variables from the context in which they are defined.” What this means is that any captured constant or variable will create a strong reference that ARC will maintain for the duration of the life of the closure, which could create a strong reference cycle.
The most common risk is creating a strong reference to self
inside of a closure. For instance, in Kamp Kit iOS, we pass closures into the BreedModel. We use these closures to update the view when the data changes. In a larger, more complex app, a situation might arise where we want to deinitialize the ViewController, but not the BreedModel. The problem is that the closure captures self
- meaning the ViewController - so the ViewController cannot be deinitialized without specifying that the reference to self is weak.
Weak References
You can tell ARC that inside of your closure you want weak references to self instead of strong ones. You do this by declaring [weak self]
at the very beginning of your closure, before any parameter name declarations or anything else. THAT’s how important it is. This essentially makes self
optional (Swift for nullable) inside of your closure.
Escape from Current Context
For those wondering how or why Swift has these language features that can lead to reference cycles, a brief moment in Swift’s defense. Swift has a concept of escaping vs non-escaping closures.
An escaping closure is one that is passed as an argument to a function, but may be called after that function returns. These are the types of closures you have to be careful with, and they’re used frequently in KMP projects.
Non-escaping closures are the default behavior in Swift, and tend to be more straightforward. Because they cannot escape their current context, they’re more associated with synchronous code and code that doesn’t capture references outside of its own scope. The sort(by:)
and filter(_:)
functions contain good examples of non-escaping closures.
The Swift compiler will try to ensure that you know you’re working with an escaping closure by forcing you to mark them with the @escaping
as a prefix to their type declaration. If you’re working on a KMP project and you’re not yet comfortable navigating Xcode, it can be difficult to confirm whether or not the closures you originally wrote in Kotlin are escaping or not, but you can check. Opt + Click one of the constructor’s parameter names to see its Swift declaration for yourself.
In KaMPKit, we pass in viewUpdate(for:) and errorUpdate(for:) closures into the BreedModel’s constructor. The constructor is going to return/complete before those closures are ever called. Consequently, these closures must be escaping and require [weak self]
.
Guardians of the Nullability
In Swift, like in Kotlin, nullability is represented with a ?
. So after declaring [weak self]
at the beginning of your closure, all of the references to self inside of the closure are nullable and must be checked or unwrapped before use. Some developers accomplish this by immediately declaring guard let self = self else \{ return \}
as the first line of a closure that declares [weak self]
.
Without going too much into Swift guard statements (which are super cool), just know that this makes your reference to self into a strong reference for everything after it in its current scope IF self is not nil. If self is nil then the else
statement is executed and the closure returns. Nothing after the else
is executed.
If the self = self
statement makes you uneasy, know that it’s an officially supported Swift language feature, not a loophole or hack. You could also always do something more like this, to be as absolutely safe as possible:
Many, if not most, native iOS developers would probably find that excessive. The print
statement in particular is the Swift equivalent of driving with two hands on the wheel, leaning forward, 5 mph under the limit.
The difference between unwrapping self and using optionals is more than just personal preference. If self is nil and you use a guard statement, then no code after the else statement will be executed. However, if you use self optionally in your closure, the lines of code in your closure without references to self will still be executed.
Conclusion
Swift is an essential part of the Kotlin Multiplatform mobile environment, so it’s in your and your multiplatform mobile project’s best interests to learn some of the finer points and best practices of Swift. Familiarity with concepts such as escaping closures and ARC will help you avoid some of the most common iOS-side mistakes in KMP projects. Those concepts are great jumping off points for learning more Swift. It’s a truly fantastic language, capable of producing some of the most elegant and readable code that’s out there.
Closures, escaping, and preventing reference cycles in general are highly visible discussion topics in the S/O Swift community. There are too many posts and answers to link here, but I strongly recommend that community at large as a resource.
- https://docs.swift.org/swift-book/LanguageGuide/Closures.html
- https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html#ID56
- https://medium.com/flawless-app-stories/you-dont-always-need-weak-self-a778bec505ef
- https://www.swiftbysundell.com/articles/capturing-objects-in-swift-closures/
- https://medium.com/@bestiosdevelope/what-do-mean-escaping-and-nonescaping-closures-in-swift-d404d721f39d
- https://fuckingclosuresyntax.com/
- Wholesome version: http://goshdarnclosuresyntax.com/