Closures in Swift are a great feature and are useful for tasks such as network callbacks, notification subscription, and providing an alternative to the delegate pattern.
Recently, I discovered that code as inconspicuous as a &&
(and) conditional could also leverage this feature.
Let's assume that we are modelling a User
of a global subscription-based application where users can customize their theme color.
We could represent this in Swift as a struct that has properties for its: identifier, subscription status, country code, and theme color.
The resulting User
model could look like this:
struct User {
var id: String
var isSubscribed: Bool
var themeColor: UIColor?
var countryCode: String
}
We'll make the themeColor
optional here since it'll be up to the User if they want to customize this.
Assume that our application already had an object to represent a theme called Theme
:
class Theme {
let backgroundColor: UIColor
init(backgroundColor: UIColor?) {
self.backgroundColor = backgroundColor ?? .white
}
}
Observe that this init
will fallback to UIColor.white
(or .white
) if the backgroundColor
property is not provided.
Given this knowledge, we could create a new class just for the custom User theme.
We can initialize the custom theme with a User in order to grab its themeColor
. The theme color passes to its parent class as its backgroundColor
.
A Theme
subclass can represent the custom User theme object:
class UserTheme: Theme {
let user: User
init(user: User) {
self.user = user
super.init(backgroundColor: user.themeColor)
}
}
Let's assume that we have received additional requirements that limit User theming to:
- subscription users
- Canadian users
We could then update our init
method on UserTheme
to handle these cases as follows:
var themeColor: UIColor?
if self.user.isSubscribed && self.user.countryCode == "CA" {
themeColor = self.user.themeColor
}
super.init(backgroundColor: themeColor)
Although this looks alright, it'll actually fail to compile. You'll see the following error log in Xcode:
error: 'self' captured by a closure before all members were initialized
if self.user.isSubscribed && self.user.countryCode == "CA" {
^
This error message while informative is a bit confusing. What closure is it referencing?
We could easily fix by removing the explicit self
and instead rely on the user
property passed in as a parameter. This fixes the symptom and not the root cause which is that there is an implicit closure in this line of code?
After posting this error message to the tacow Slack group, @rydermackay pointed out to me that the Swift language is capturing the right-hand side of the conditional in a closure.
That is, given the conditional: lhs && rhs
("lhs" and "rhs" represent Left-Hand Side and Right-Hand Side respectively). rhs
is being wrapped in a closure. More specifically, it is wrapped in an autoclosure
so that it could lazily evaluate the right condition if the left condition was false.
This can be shown by looking at the source code for the && operator:
public static func && (lhs: Bool, rhs: @autoclosure () throws -> Bool) rethrows -> Bool {
return lhs ? try rhs() : false
}
In order to understand the warning a little better, we need to know what an autoclosure
is.
From the Apple documentation, an autoclosure
:
...is a closure that is automatically created to wrap an expression that’s being passed as an argument to a function. It doesn’t take any arguments, and when it’s called, it returns the value of the expression that’s wrapped inside of it.
but most importantly:
An autoclosure lets you delay evaluation because the code inside isn’t run until you call the closure. Delaying evaluation is useful for code that has side effects or is computationally expensive because it lets you control when that code is evaluated.
That means that the above &&
implementation at a high level is equivalent to the following. Note: We can't actually write &&
without the rhs
since it's defined to have both parameters.
// For simplicity, I've removed all throws
public static func && (lhs: Bool, rhs: () -> Bool) -> Bool {
return lhs ?? rhs()
}
// Example: Assume A and B are some boolean conditions
A && { () -> Bool in
return B
}
If we update our previous UserTheme
init
with this re-interpretation, it would look like this
let result = self.user.isSubscribed && { () -> Bool in
return self.user.countryCode == "CA"
}
if result {
themeColor = self.user.themeColor
}
As you can see, the block captures self.user.countryCode == "CA"
and since we're doing this before super.init()
when all members have initialized the compilation fails.
There are a few ways to fix this, we could:
- move all this offending code to below the
super.init()
- store the result of the right-hand side expression in a separate variable, or;
- we could move the creation of Themes to a factory class to avoid a
Theme
subclass
Either way, these all work but in general you should typically avoid referencing self
before the super.init()
in cases like these.
I hope you learned something and potentially got you interested in looking at more implementation details of the Swift programming language.
The sample code can be found here.
Additional Resouces
Originally posted on my website.
Top comments (0)