RFC: Multiple Trailing Closures One Year Later

So this feature is now older the one year (I count from the pitch and the discussion date not from the actual release date). I would like to ask the community about the adoption of this feature in their code bases.

Have you adopted / used 'multiple trailing closures'?

  • I am using it.
  • I am using it but I wish we had the optional first label.
  • I only tried it out.
  • I would use it if it would allow to explicitly use the first closure's label.
  • I am not using it at all.
  • Never heard of it.

0 voters

Feel free to also leave more detailed feedback down thread or suggest for better poll options.

9 Likes

I don't have any functions that take more than one closure in my codebase

1 Like

What’s the point in voting?

I remember it was a fairly controversial moment in the history of Swift Evolution. To risk a rematch wouldn’t be particularly healthy for the community unless there was some good intention.

Plain curiosity and maybe a potential signal to revive the discussion for an optional label for the first closure. My intention is not to undo anything, but to potentially extend this feature as IIRC the first closure's label wasn't fully ruled out if it would be made optional.

And to be clear with everyone else. This thread should not tolerate or welcome any toxicity around this topic. The intention is just to have a normal and productive conversation with the active community members.

13 Likes

Sorry to revive this but, has there been any further discussion about introducing the optional first label? I believe the results of the poll suggest the community wishes for a revision of this feature.

You can find a proper pitch in Support use of an optional label for the first trailing closure - #14 by xwu.

2 Likes

Thank you!

I'm voting for optional first label, because there cases where it is need for readability. Some examples:
1.

cell.setProductsList(products, dsInput) { instanceId in
  
} changeAmountClosure: { amountParams in
  
} noAddressClosure: {
  
} restrictedByAgeClosure: {
  
}

Here first closure is named 'selectClosure', but we need to see function declaration if we want to know what is it.

  1. RxSwift do() and subscribe() functions. The declaration is:
func `do`(onNext: ((Void) throws -> Void)? = nil,
          afterNext: ((Void) throws -> Void)? = nil, 
          onError: ((Error) throws -> Void)? = nil, 
          afterError: ((Error) throws -> Void)? = nil, 
          onCompleted: (() throws -> Void)? = nil, 
          afterCompleted: (() throws -> Void)? = nil, 
          onSubscribe: (() -> Void)? = nil, 
          onSubscribed: (() -> Void)? = nil, 
          onDispose: (() -> Void)? = nil) -> Observable<Void>

In most cases we use .do(onNext: {}) or .do(afterNext: {}), but sometimes we use .do(afterNext: {}, onCompleted: {}), so we need to specify first argument explicitly.

2 Likes

So I tried to use multiple trailing closures for the first time today, and the inability to specify a label for the first closure really hurts.

Consider SwiftUI's GroupBox. This is how it generally looks when using a simple text label:

GroupBox(label: Text("Some Group")) {
  TextField(...)
    .blah()
    .blah()
    .blah()
}

This works well, in particular because it mirrors how group boxes actually look - with their labels on top:

It's also nice for people reading the code - it serves sort of like a divider, like a // MARK: - Some Group comment, telling people as they read the code (from top to bottom), that we're entering the portion which defines the content of "Some Group".

Now, I wanted to use a somewhat flashier GroupBox label, with a button to the side. My first thought was to write something like this, replacing the in-line Text with a @ViewBuilder:

GroupBox {
  HStack {
    Text("Some Group")
    Spacer()
    FlashyControl()
  }
} content: {
  TextField(...)
    .blah()
    .blah()
    .blah()
}

The added complexity does make it a little less clear and dilutes some of the benefits that we used to have, but it's not too bad as long as the label remains relatively noncomplex.

But what I've found is that, when the label is provided by a @ViewBuilder, the SwiftUI team have reversed the order of the arguments, so the content comes first, and the label comes afterwards. It feels backwards, and it is backwards compared to GroupBox's other initializers. So now what I need to write is this:

GroupBox {
  TextField(...)
    .blah()
    .blah()
    .blah()
} label: {
  HStack {
    Text("Some Group")
    Spacer()
    FlashyControl()
  }
}

I can only speculate as to why they've made this decision (if they're reading this, please confirm/deny), but my guess would be that, in a language where the first closure cannot be labelled, in some way it becomes more intuitive for that closure to contain the body of the GroupBox. That's sort of the "main thing", and since you don't have the label to make it clear which is which, it's sort of clearer? Kind of? Ish?

But in any case, I think the result is fairly poor. It doesn't read well, and actually feels wrong. The thing I would really love to see is this:

GroupBox label: {
  HStack {
    Text("Some Group")
    Spacer()
    FlashyControl()
  }
} content: {
  TextField(...)
    .blah()
    .blah()
    .blah()
}

Alternatively, I think something like this, with an analogous structure to a switch statement, would look even nicer and clearer:

GroupBox {
label:
  HStack {
    Text("Some Group")
    Spacer()
    FlashyControl()
  }
content:
  TextField(...)
    .blah()
    .blah()
    .blah()
}

But yeah, not even having the option to specify a leading label isn't very nice. It looks so wrong that I'm not even going to use the @ViewBuilder overload provided by SwiftUI - I'm just going to write my own helper which uses the label-first order, and enclose it in parenthesis rather than use trailing closures.

21 Likes

I really wish a label for the first closure was added in at least Swift 6. There are cases where it's just impossible to use multiple trailings closure and not end up with something that looks strange and confusing.

15 Likes

Would be lovely if you all could write down these real-world examples. All the other ingredients are there for this issue to go through the Evolution process.

11 Likes

This one really bugs me:

.fullScreenCover(isPresented: onDismiss: content:)

if the first closure was named, it'd be heaps better:

.fullScreenCover(isPresented: $viewModel.showingSomething)
onDismiss: {
    // some code
} content: {
    // content
}
2 Likes

I’d be in favor of allowing labels for singular trailing closures as well.

reversedNames = names.sorted by: { $0 > $1 }

This could also be used to disambiguate between functions that vary only by the closure's argument label.

6 Likes

In fact, I see that is already covered by @xwu’s pitch.

I suspect it could actually have a bigger impact, at least if you exclude SwiftUI and usage thereof. Functions with multiple trailing closures aren’t particularly common.

1 Like

It’s not exactly a breaking change, it could be added in a minor release.

This is my most requested change in Swift for years. Since I find it an unnecessary ugly part of Swift.

There is already a very good pitch, what is hindering the pitch to be a Swift Evolution Proposal?

Is there anything I can do to advance the evolution process?

7 Likes