`String.StringInterpolation` extension use and competing `ExpressibleByStringInterpolation`

I'm struggling a bit with what the best way is to use custom string interpolators in a context that takes both a ExpressibleByStringInterpolation or a StringProtocol. As an example, let's imagine a SwiftUI Button:

Button("50%") {}

Now let's imagine we have a custom string interpolator:

public extension String.StringInterpolation {
    mutating func appendInterpolation(percent ratio: some BinaryFloatingPoint) {
        self.appendLiteral(Self.percentFormatter.string(for: ratio) ?? "")
    }
    private static let percentFormatter = using(NumberFormatter()) {
        $0.numberStyle = .percent
    }
}

We could now ostensibly turn our Button into:

Button("\(percent: rating)") {} // Extraneous argument label 'percent:' in call

Alas, we now get a bizarre error message (y'all, swiftc's error messages need a lot of love).
After a lot of research we discover that, in fact, the problem is unrelated to our interpolators or their argument labels, but rather, Button has two competing initializers:

init(_ titleKey: LocalizedStringKey, action: @escaping () -> Void)
init<S>(_ title: S, action: @escaping () -> Void) where S : StringProtocol

Where String : StringProtocol and LocalizedStringKey : ExpressibleByStringInterpolation. The use of the interpolation appears to have shifted the compiler over to trying to use a different initializer than before, and failing to do so, since the interpolations available in that context do not support the arguments provided.

  1. Why did swiftc not continue to look for solutions to the requirements written in code; such as a String (supporting StringProtocol) which is also ExpressibleByStringInterpolation?
  2. What is the recommended approach to disambiguating this use case or writing this in plain code which compiles and runs as expected?

Note, the following hint is enough to bop the compiler into, "OH, THAT'S WHAT YOU MEAN":

Button("\(percent: rating)" as String) {}
1 Like