Swift 5.3 and Trailing Closures

As I write this, it’s still less than three months since WWDC 2020, and Swift 5.3 is still being licked into shape. It’s not terribly revolutionary; here are a few highlights:

  • @main instead of @UIApplicationMain, and the ability to declare a @main struct in place of a main.swift file (SE-0281)

  • Synthesis of Comparable conformity for enums (SE-0266)

  • Relaxed requirements for explicit self in escaping anonymous functions (SE-0269)

  • Where clauses for individual methods (SE-0267)

But in my view, the greatest new feature of Swift 5.3 is support for multiple trailing closures. It appears that this has turned out to be trickier to implement than originally supposed; SE-0279 was present in Xcode 12 beta 1, but things still didn’t work quite right, and SE-0286 was moved along briskly so as to make it into Xcode beta 5.

This is a stylistic change — perhaps you could call it mere syntactic sugar. But it will make for cleaner code; Swift will be easier to write and easier to read because of it.

What It Used to Be Like

The straw man for demonstrating the problem is always UIView animation, so let’s use that. Return with me now to the nostalgic days of Swift 5.2. Here’s a call to a commonly used method:

UIView.animate(withDuration: 2, delay: 0, options: [], animations: {
    // ...
}, completion: { _ in
    // ...
})

That way of writing the call has the advantage of perfect parallelism. Every parameter is preceded by a label, and all parameters are contained within the parentheses of the call. Nonetheless, that isn’t very Swifty. Swift has trailing closure syntax. The natural thing is to move the completion: function out of the parentheses:

UIView.animate(withDuration: 2, delay: 0, options: [], animations: {
    // ...
}) { _ in
    // ...
}

The problem, if you want to call it that, is the lack of parallelism. There’s the animations: parameter locked up inside the parentheses, while the completion: parameter is allowed to roam free outside them. But it gets worse; if you omit the completion: parameter entirely, suddenly the animations: parameter is allowed to use trailing closure syntax:

UIView.animate(withDuration: 2) {
    // ...
}

That’s where things start to get a little upsetting. Exactly what is that thing in the curly braces? Is it the animations: parameter or the completion: parameter? How do you know? Well, in this case, you do know, because if this were the completion: parameter, the compiler would be complaining that we’re not dealing with the completion: function’s single Bool parameter. Still, it’s potentially confusing.

And here’s the really terrible part. In that example, I deliberately took away the delay: and options: parameters. If I put them back again, we don’t compile any longer:

UIView.animate(withDuration: 2, delay: 0, options: []) { // compile error!
    // ...
}

The compiler is now throwing a wobbly. It seems it thinks this is the completion: parameter now; it has some other ideas about what’s wrong with this code as well. But the question in the mind of a Swift programmer is: how can the introduction of more of the optional parameters suddenly change the meaning of the trailing closure?

The truth is that that’s a very weird case because the method we’re trying to call comes from Cocoa Objective-C — which has no optional parameters! There are actually two methods here, and the compiler is simply choosing between them. Swift, however, does have optional parameters. So at the risk of pressing the point too hard, let me make up another edge case. Here’s a method that takes two optional functions:

func test(first: (()->())? = nil, second: (()->())? = nil) {
    first?()
    second?()
}

Now I’ll call it with a single trailing closure:

self.test {
    // ...
}

Well? What is that trailing closure? Is it first: or second:? How do you know? Okay, yes, it’s second:, you’re right. But be honest: weren’t you just guessing?

The New Dispensation

The new rule in Swift 5.3 (SE-0279) is that if a function’s final parameters are functions, all of them can be trailing closures. The syntax is that the first trailing closure gets no label; the others get a label with no preceding comma:

UIView.animate(withDuration: 2, delay: 0, options: []) {
    // ...
} completion: { _ in
    // ...
}

Perhaps that’s a little surprising, not least because if you’re used to using trailing closures, you probably don’t know the name of the last parameter and you’re not accustomed to seeing it. Nevertheless, it does get both functions out of the parentheses.

Granted, it is still not completely parallel; the animations: function has no label, but the completion: function does. A truly parallel syntax would allow us to make the animations: label explicit, even with a trailing closure; but the compiler won’t let us do that:

UIView.animate(withDuration: 2, delay: 0, options: []) animations: { // compile error
    // ...
} completion: { _ in
    // ...
}

What about the situation when there is no completion: parameter? When Swift 5.3 first appeared in Xcode 12 beta 1, things hadn’t changed. This was legal:

UIView.animate(withDuration: 2) {
    // ...
}

But this wasn’t:

UIView.animate(withDuration: 2, delay: 0, options: []) { // compile error
    // ...
}

So things were still confusing, and this greatly limited one’s willingness to use trailing closure syntax at all. The Swift folks took this situation seriously, and fast-tracked another proposal (SE-0286), so that it was incorporated into Xcode 12 beta 5. This is a profound change that reverses the order in which the compiler examines the function parameters; it uses “forward matching” instead of “backward matching”. The consequence is that both expressions are now legal, and they both do what you expect them to do; the single trailing closure is the animations: parameter.

But what about my edge case? Recall that this is a Swift function that takes two optional functions:

func test(first: (()->())? = nil, second: (()->())? = nil) {
    first?()
    second?()
}

And I call it with a single trailing closure:

self.test {
    // ...
}

What happens now is that the compiler slaps your hand. Your code still compiles, and this is still the second: function, but you get a warning: “Backward matching of the unlabeled trailing closure is deprecated; label the argument with ‘second’ to suppress this warning.”

But what does the compiler mean by this warning? What are we supposed to do? The fact is that there is no satisfactory solution in this situation. You cannot just label the trailing closure, as the warning seems to suggest. This is illegal, as I’ve already said:

self.test() second: { // compile error
    // ...
}

One option is to include the first parameter explicitly, like this:

self.test(first:nil) {
    // ...
}

That’s legal because there is now only one trailing closure, so it isn’t labeled, and it’s clear which one it is. But now we’re back to Swift 5.2, with one function inside the parentheses and the other outside them. Another possibility is to have no trailing closures at all:

self.test(second: {
    // ...
})

But if we’re going to do that, what was all the fuss about? Why do we have trailing closures in the first place, if we’re not going to use them? In a sense, this no advance over where we were before. In my view, what should happen here is that the “forward scan” should match the single trailing closure to the first: parameter, with no warning — just like what happens with UIView.animate.

My impression from reading the SE-0286 proposal, though, is that the Swift people know that this is an issue, and that what I’m seeing is a temporary stop-gap solution to give people time to transition to the new dispensation — at which point, in Swift 6, the single trailing closure will be the first: parameter and all will be pretty much right with the world.

Overall Judgment

In my view, the new world order for trailing closure syntax is a better world order. I like trailing closure syntax, and I think my code looks a lot better with it. I converted all my code so that now I use trailing closures, and in particular multiple trailing closures, everywhere possible.

For instance, I used to have this (making a new photo album and retrieving its identifier):

var ph : PHObjectPlaceholder?
PHPhotoLibrary.shared().performChanges({
    let t = "TestAlbum"
    typealias Req = PHAssetCollectionChangeRequest
    let cr = Req.creationRequestForAssetCollection(withTitle:t)
    ph = cr.placeholderForCreatedAssetCollection
}) { ok, err in
    if ok, let ph = ph {
        self.newAlbumId = ph.localIdentifier
    }
}

The first function was inside the parentheses. Now I’m writing it like this:

var ph : PHObjectPlaceholder?
PHPhotoLibrary.shared().performChanges {
    let t = "NewAlbum"
    typealias Req = PHAssetCollectionChangeRequest
    let cr = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle:t)
    ph = cr.placeholderForCreatedAssetCollection
} completionHandler: { ok, err in
    if ok, let ph = ph {
        self.newAlbumId = ph.localIdentifier
    }
}

I think stuff like that is clean and clear. After converting my code, I did a massive global search looking for closures that were still inside parentheses, and the only remaining cases are those where there really needs to be a closure inside parentheses. For instance:

self.button.addInteraction(UISpringLoadedInteraction { int, con in
    // ...
})

In that code, I’m calling the UISpringLoadedInteraction initializer inside the call to addInteraction. Therefore it is right that the UISpringLoadedInteraction initializer’s trailing closure should be followed by the addInteraction call’s closing parenthesis. It’s two different delimiters belonging to two different calls! And if I really don’t like it, I can always rewrite in two steps:

let interaction = UISpringLoadedInteraction { int, con in
    // ...
}
self.button.addInteraction(interaction)

The really important thing is that I now feel confident about the correctness of my code without even bothering to compile. Previously, edge cases like this one threw me for a loop:

UIView.animate(withDuration: 2, delay: 0, options: []) { // compile error!
    // ...
}

The result was that I was pretty much scared to use trailing closures at all because I never knew when I’d hit a forbidden edge case. Now, there are basically no forbidden edge cases; things feel natural and deterministic.

You Might Also Like…

Swift 5.5: Replacing GCD With Async/Await

Multithreading! The mere word sends shivers up one’s spine. And if it doesn’t, it should. Main thread and background threads. Code that runs asynchronously. Code that can run simultaneously with other code. Code that can run simultaneously with itself. Code that can share data across threads — possibly with disastrous consequences. Concurrency. Multithreaded code is …

Swift 5.5: Replacing GCD With Async/Await Read More »

    Sign Up

    Get more articles and exclusive content that takes your iOS skills to the next level.