Principles for Trailing Closure Evolution Proposals

It seems that Apple's solution to the designability and usability issues I pointed out earlier is to overload APIs which use multiple closures. For example, the new Gauge SwiftUI view on watchOS 7 has 5 overloads for its initializer:

init<V>(value: V, in: ClosedRange<V>, label: () -> Label)
init<V>(value: V, in: ClosedRange<V>, label: () -> Label, currentValueLabel: () -> CurrentValueLabel)
init<V>(value: V, in: ClosedRange<V>, label: () -> Label, currentValueLabel: () -> CurrentValueLabel, markedValueLabels: () -> MarkedValueLabels)
init<V>(value: V, in: ClosedRange<V>, label: () -> Label, currentValueLabel: () -> CurrentValueLabel, minimumValueLabel: () -> BoundsLabel, maximumValueLabel: () -> BoundsLabel)
init<V>(value: V, in: ClosedRange<V>, label: () -> Label, currentValueLabel: () -> CurrentValueLabel, minimumValueLabel: () -> BoundsLabel, maximumValueLabel: () -> BoundsLabel, markedValueLabels: () -> MarkedValueLabels)

Personally, I think this approach is deeply flawed, from both designability and usability standpoints. But fundamentally I think it indicates that the post-0279 syntax isn't up to the job of producing APIs using multiple closures. Requiring overloading to produce a usable API places a huge burden on API designers, both in initial work and long term maintenance. I think it's more likely most designers either won't implement such overloads, making for a subpar user experience, or they'll avoid multiple closure APIs altogether. Neither is good for the community.

I would hope that Apple's own experience creating multiple closure APIs would've produced the experience necessary for further consideration on this topic, but that seems unlikely at this point.

@Ben_Cohen Are there any more details to share about the path forward here, or any response to the various issues raised in this thread?

46 Likes

To be clear, addressing SE-0279 in the way you are describing would not eliminate these overloads from SwiftUI. You've omitted the generic constraints, which are important here. The first two overloads are more like this, in context:

struct Gauge<Label: View, CurrentValueLabel: View> {
  init<V>(value: V, in: ClosedRange<V>, label: () -> Label) where CurrentValueLabel == EmptyView
  init<V>(value: V, in: ClosedRange<V>, label: () -> Label, currentValueLabel: () -> CurrentValueLabel)
}

If you took away the first overload, there would be no way to write a default parameter for currentValueLabel in the current language that would infer a generic argument for CurrentValueLabel.

It's not that your critique is invalid, but this API is not an example of the problem you describe.

Doug

2 Likes
Terms of Service

Privacy Policy

Cookie Policy