swiftuilocalizedstringkey

How to support localization automatically in SwiftUI using LocalizedStringKey?


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.


Solution

  • 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().