I'm trying to create a custom SwiftUI view that can be initialized with either a LocalizedStringKey
or StringProtocol
just like the built-in SwiftUI Button
. The benefit is that I can initialize my custom view with either a string that automatically gets localized, or a parameter that is assumed to already be localized.
Apple describes this usage here:
As a general rule, use a string literal argument when you want localization, and a string variable argument when you don’t.
Source: https://developer.apple.com/documentation/swiftui/localizedstringkey
Here is my code:
import SwiftUI
struct CustomView<Content: View>: View {
private let title: String
@ViewBuilder let content: Content
init(_ titleKey: LocalizedStringKey, @ViewBuilder content: () -> Content) {
self.title = ???
self.content = content()
}
init<S>(_ title: S, @ViewBuilder content: () -> Content) where S : StringProtocol {
self.title = ???
self.content = content()
}
var body: some View {
ZStack {
Color.green
Text(self.title)
}
}
}
It's unclear in either initializer how I should set the title
field that gets used in the body
.
I assume there is some pattern that Apple is using here, but it's not really clear how to achieve this.
I realize that I could just store two variables in the custom view; one for the LocalizedStringKey
and the other for the String
, and then have a switch that uses the appropriate input type in the body
. But this seems like slightly more work than I would expect.
Any help is appreciated.
Use Text
.
You won't have trouble passing a Text
to other SwiftUI views. All the SwiftUI types that can take a LocalizedStringKey
or StringProtocol
, would also have an overload that either takes a @ViewBuilder
(e.g. the label of a Button
) or just a plain Text
(e.g. label of a TableColumn
or SharePreview
).
let content: Content
let title: Text
init(_ titleKey: LocalizedStringKey, @ViewBuilder content: () -> Content) {
self.title = Text(titleKey)
self.content = content()
}
@_disfavoredOverload
init<S>(_ title: S, @ViewBuilder content: () -> Content) where S : StringProtocol {
self.title = Text(title)
self.content = content()
}
You should also mark the StringProtocol
overload with @_disfavoredOverload
, so that passing a string literal will resolve to the LocalizedStringKey
overload.
SwiftUI itself probably also uses Text
to do this, as we can see that the built-in view initialisers that take LocalizedStringKey
/StringProtocol
typically require the Label
type parameter of that view to be Text
.
In general, the pattern looks something like this:
struct FooView<Label: View>: View {
private let label: Label
init(_ titleKey: LocalizedStringKey) where Label == Text {
self.init {
Text(titleKey)
}
}
@_disfavoredOverload
init<S>(_ title: S) where S : StringProtocol, Label == Text {
self.init {
Text(title)
}
}
init(@ViewBuilder label: () -> Label) {
self.label = label()
}
var body: some View { ... }
}
For views that can work with any kind of label, it would have a @ViewBuilder
initialiser like the above. If your view only supports text, then it would have an initialiser that takes Text
. Consider adding such an overload to your view too. This allows users of your view to pass you styled text, by using things like .bold()
, .font()
, .foregroundStyle()
.