StringProtocol type inference issue in SwiftUI's Label initializers
gongzhang opened this issue Β· comments
I just noticed a strange behavior. In SwiftUILabelExtension
, the protocol StringProtocol
is always matched to String
instead of LocalizedStringKey
if you write a string literal in your code. This is inconsistent with other SwiftUI native methods. For example:
// Native SwiftUI:
Text("some text") // inferred as LocalizedStringKey
Label("some text", systemImage: "...") // inferred as LocalizedStringKey
.navigationBarTitle("some text") // inferred as LocalizedStringKey
// SwiftUILabelExtension:
Label("some text", systemSymbol: .xxx) // hmm... inferred as String... π€
So I have to do the following to ensure the text is being localized correctly:
Label("some text" as LocalizedStringKey, systemSymbol: .xxx) // π€
I checked the implementation of SwiftUILabelExtension
and it seems to be defined in the same way as in SwiftUI's native methods, but I don't know why the compiler doesn't give priority to matching LocalizedStringKey
.
public extension Label where Title == Text, Icon == Image {
/// Creates a label with a system symbol image and a title generated from a
/// localized string.
///
/// - Parameter systemSymbol: The `SFSymbol` describing this image.
init(_ titleKey: LocalizedStringKey, systemSymbol: SFSymbol) {
self.init(titleKey, systemImage: systemSymbol.rawValue)
}
/// Creates a label with a system symbol image and a title generated from a
/// string.
///
/// - Parameter systemSymbol: The `SFSymbol` describing this image.
init<S>(_ title: S, systemSymbol: SFSymbol) where S : StringProtocol {
// π€ always goes here with S==String...
self.init(title, systemImage: systemSymbol.rawValue)
}
}
I suggest to check if we can find a better way to define the initializer signatures. Because this inconsistent behavior can easily lead to localization bugs, and it is less easy to be found if the app is not fully tested.
@gongzhang Thanks for reporting, this is a weird issue indeed.
I have pushed work/swiftui-localize
and although it's rather unlikely that that change will fix the issue, it would help a lot if you could try that branch and report whether that makes a difference. Thanks in advance!
@fredpi Thanks for the reply! I tried it and it didn't work. Then I fiddled with the code snippet below in the Xcode Playground for a while, but string literal always fails to match LocalizedStringKey
by default.
// Xcode Playground
import SwiftUI
public extension Label where Title == Text, Icon == Image {
init(_ titleKey: LocalizedStringKey) {
print("1")
self.init(titleKey as LocalizedStringKey, systemImage: "1.circle")
}
init<S>(_ title: S) where S : StringProtocol {
print("2")
self.init(title, systemImage: "2.circle")
}
}
// always match no.2 intializer, never goes to no.1.
// ... unless remove the no.2 completely
Label("xxx")
I'm now a bit suspicious that the compiler might have taken some special care for SwiftUI. Or maybe I should take a look at the source code of SwiftUI.π
@gongzhang Thanks! The playground helps understanding that the issue is about the the wrong initializer being selected. I guess what we're observing is quite non-deterministic behavior:
- Every
StringProtocol
andLocalizedStringKey
conform toExpressibleByStringLiteral
, so the type of an expression like "abc" can only be inferred based on where it gets used. - As there are both an initializer for
StringProtocol
and forLocalizedStringKey
, that doesn't help with inferring the proper type. - Swift apparently just selects
String
as the type when using the SFSafeSymbols initializer andLocalizedStringKey
when using the original initializer. There seem to be no deterministic reasons for that.
Interestingly, for SwiftUI's Text
type, there is a differentiation, see Apple's documentation. There we have two distinct initializers (distinct because of the verbatim
argument label):
init(_ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil)
init(verbatim content: String)
I suggest the following:
- Adjust SFSafeSymbols' initializers argument labels from
_
totitleKey
ortitle
respectively. - File feedback with Apple about this (especially the inconsistency compared to the
Text
type)
Do you agree with my assessment? Would you mind doing the second task?
@fredpi I agree with you. π€
- Maybe we can keep
init(_:systemSymbol:)
and only updateinit<S>(_:systemSymbol:)
toinit<S>(verbatim:systemSymbol:)
? The effect of this modification is as follows.
Label("Abc") // LocalizedStringKey, good! It is the expected.
let string: String = "some non-localization text"
Label(str) // compiler error: Missing argument label "verbatim:" in call - Fair enough.
Label(verbatim: str) // Acceptable, also more clarity.
What do you think?
- I will continue to research this topic then I'll be back to update :)
@gongzhang Did you find anything new about this issue?
If not, I'd go on and change the StringProtocol
initializer to Label(verbatim:)
as you suggested.
@fredpi Sorry for the lack of updates for a while, as I didn't find any useful information. π¬
@gongzhang @_disfavoredOverload
is the solution, as @ddddxxx found out and implemented in #64.
Here's an article about it that even refers to the SwiftUI initializers SFSafeSymbols
replicates: https://fivestars.blog/swift/disfavoredOverload.html
@fredpi That's brilliant! Thank you so much π