A source breaking case for ForwardTrailingClosures in Swift 6

When preparing my Swift 5.8 code to Swift 6 using -enable-upcoming-feature ForwardTrailingClosures

I noticed a source breaking change in some framework.

extension View {
    public func sheet<Content>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Content) -> some View where Content : View
}

Before I enable this feature, the following code compiles (with onDismiss default to nil, the closure I provide is for content):

xx.sheet(isPresented: $editingItem) {
    Text("")
}

However after I enable this feature, the following code will give me an error saying Content could not be inferred. And I have to change it to the following :point_down:

xx.sheet(isPresented: $editingItem) {
    
} content: {
    Text("")
}

or

xx.sheet(isPresented: $editingItem, onDismiss: nil) {
    Text("")
}

All the upcoming features are not enabled in Swift 5 because they’re source-breaking, IIRC. So source breakages are expected (or else we can enable them right away!).

For the specific case, I felt that whether trailing closures are matched due to its effect on call site. The case you encountered is matched with SE-0286, but it indeed looks unnatural. I think the following pattern will look a little bit better:

xx.sheet(isPresented: $editingItem, onDismiss: nil) {
    Text("")
}

But it’s still annoying. It seemed the backward scanning rule was designed because we once had trailing closure, so API designers naturally put whichever parameter they want user to use the syntax at the back. Now that multiple trailing closures are introduced, along with the forward scanning rule, it feels that in Swift 6 we’ll eventually change the previous practice, arranging closure inputs according to priority in descending order. In this case, it should be:

extension View {
    @_disfavoredOverload // deprecation is up to your need
    public func sheet<Content>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Content) -> some View where Content : View

    public func sheet<Content>(isPresented: Binding<Bool>, @ViewBuilder content: @escaping () -> Content, onDismiss: (() -> Void)? = nil) -> some View where Content : View
}
2 Likes

Yes, source-breaking is expected as I manually opt-in the Swift 6 feature.

This is what I currently use for now.

xx.sheet(isPresented: $editingItem, onDismiss: nil) {
    Text("")
}

The problem is framework author need to redesign their API if it was target in Swift 6 which makes me feeling uncomfortable.

extension View {
    @_disfavoredOverload // deprecation is up to your need
    public func sheet<Content>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Content) -> some View where Content : View

    public func sheet<Content>(isPresented: Binding<Bool>, @ViewBuilder content: @escaping () -> Content, onDismiss: (() -> Void)? = nil) -> some View where Content : View
}