In SwiftUI the Text view comes with multiple initializers, my question is about one of them:
public init<S>(_ content: S) where S : StringProtocol
..which has the following documentation:
Creates a text view that displays a stored string without localization.
Use this intializer to create a text view that displays — without
localization — the text in a string variable.
Text(someString) // Displays the contents of `someString` without localization.
SwiftUI doesn't call the `init(_:)` method when you initialize a text
view with a string literal as the input. Instead, a string literal
triggers the ``Text/init(_:tableName:bundle:comment:)`` method — which
treats the input as a ``LocalizedStringKey`` instance — and attempts to
perform localization.
...
In short:
this initializer is used when the passed parameter is not a string literal
if we pass a string literal, another initializer is called instead.
My question is: how?
If I try to mimic the same API in a new view, the StringProtocol initializer above always takes priority:
struct MyView: View {
let title: Text
init<S: StringProtocol>(_ content: S) {
self.title = Text(content)
}
init(_ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil) {
self.title = Text(key, tableName: tableName, bundle: bundle, comment: comment)
}
var body: some View {
title
}
}
struct ContentView: View {
var aString: String = "aString"
var body: some View {
VStack {
MyView(aString) // displays "aString"
MyView("my_localized_title") // displays "my_localized_title"
MyView("my_localized_title" as LocalizedStringKey) // displays the actual localization
MyView(LocalizedStringKey("my_localized_title")) // displays the actual localization
}
}
}
As shown, the only way to trigger MyView's "localized" initializer is to explicitly pass a LocalizedStringKey, which is not the case for Text:
struct ContentView: View {
var aString: String = "aString"
var body: some View {
VStack {
Text(aString) // displays "aString"
Text("my_localized_title") // displays the actual localization
Text("my_localized_title" as LocalizedStringKey) // displays the actual localization
Text(LocalizedStringKey("my_localized_title")) // displays the actual localization
}
}
}
My theory is that even in SwiftUI's Text the init<S: StringProtocol>(_ content: S) is called, and internally it's checked whether content is a string literal or not, and then swift proceeds from there (hence this discussion title).
Is this even possible? How can we mimic SwiftUI's Text behavior?
Thank you in advance!
import SwiftUI
func foo(_ s: LocalizedStringKey) {
print("literal")
}
func foo<S: StringProtocol>(_ s: S) {
print("String")
}
let n = 123.456
Text("Some \(n, specifier: "%.2f")") // this works
foo("Some \(n, specifier: "%.2f")") // compile error: extra argument 'specifier' in call
let s = "some other string"
foo(s)
LocalizedStringKey is ExpressibleByStringInterpolation, but only Text is "aware" of this capability. Calling my foo, it's treating it as a plain string literal so of course it do not know the interpolation that take a specifier.
My guess is some magic preprocessor thing parses the Text("some string literal") and turn these into Text(LocalizedStringKey("some string literal")).
I'm not 100% sure about how SwiftUI is actually doing it since I'm not seeing any explicit attribute on those initializers, but you can achieve it using the currently private @_disfavoredOverload attribute:
struct MyView: View {
let title: Text
@_disfavoredOverload
init<S: StringProtocol>(_ content: S) {
title = Text("not localized")
}
init(_ key: LocalizedStringKey) {
title = Text("localized")
}
var body: some View {
title
}
}
So this attribute is for when the compiler cannot decide which overload it should "favor". I wonder why the compile not just favor generic over "existential" in the case I just linked to?
Another question is with this attribute, it can break tie of two, what happen if there are more than two overloads? Say three? Seems it needs to take a "prirority" value to work this out?
That's very neat, thank you Stefano!
Yesterday I was looking for something exactly like this
When generating the interface for MyView, we actually don't see the @_disfavoredOverload attribute:
Our code:
public struct MyView: View {
let title: Text
@_disfavoredOverload
public init<S: StringProtocol>(_ content: S) {
print(content)
self.title = Text(content)
}
public init(key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil) {
self.title = Text(key, tableName: tableName, bundle: bundle, comment: comment)
}
public var body: some View {
title
}
}
The generated interface:
public struct MyView : View {
internal let title: Text
public init<S>(_ content: S) where S : StringProtocol
public init(key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil)
/// The content and behavior of the view.
public var body: some View { get }
}
Therefore I believe @_disfavoredOverload might be how Text accomplishes this as well!
I'm very open for more leads/suggestions/ideas, but this seems a very good start!