SwiftUI Localization of Strings inside an Array - swift

I'm trying to localize my App in English and German and everything worked for now, expect the strings inside an array apparently get not localized.
I have this array, which holds the options for my Picker:
let sorting = ["Newest First", "Oldest First"]
This is my Picker, which works correctly function wise:
Picker("Sort by", selection: $sortingSelection) {
ForEach(sorting, id: \.self) {
Text($0)
.foregroundColor(.accentColor)
.font(.footnote)
}
}
.pickerStyle(MenuPickerStyle())
And this is the correleting Localizable.strings (German):
"Newest First" = "Neueres Zuerst";
"Oldest First" = "Älteres Zuerst";
So I tried just writing it as strings, which is the easiest way to use localization now.
I also tried using the it as a variable, but this doesn't work:
let localizedString1 = "Newest First"
let localizedString2 = "Oldest First"
let sorting = [localizedString1, localizedString2]
I also saw this post:
Swift: Localize an Array of strings
but like the comment on the answer says, is there a way to get some example code? Or is there a better method now?

As stated in Text documentation :
If you intialize a text view with a variable value, the view uses the init(:) initializer, which doesn’t localize the string. However, you can request localization by creating a LocalizedStringKey instance first, which triggers the init(:tableName:bundle:comment:) initializer instead:
// Don't localize a string variable...
Text(writingImplement)
// ...unless you explicitly convert it to a localized string key.
Text(LocalizedStringKey(writingImplement))

The array defaults to type String, you can specify the type to LocalizedStringKey
let sorting: [LocalizedStringKey] = ["Newest First", "Oldest First"]
You might have to change the selection type too to make them match.

This works for me:
Picker("Select Category", selection: $transactionTypeString) {
ForEach(transactionTypes, id:\.self) {
Text(LocalizedStringKey($0))
}
}

Related

Build Recursive Text View in SwiftUI

My goal is to create a SwiftUI view that takes a String and automatically formats that text into Text views. The portion of the string that needs formatting is found using regex and then returned as a Range<String.Index>. This can be used to reconstruct the String once the formatting has been applied to the appropriate Text views. Since there could be multiple instances of text that needs to be formatted, running the formatting function should be done recursively.
struct AttributedText: View {
#State var text: String
var body: some View {
AttributedTextView(text: text)
}
#ViewBuilder
private func AttributedTextView(text: String) -> some View {
if let range = text.range(of: "[0-9]+d[0-9]+", options: .regularExpression) {
//The unattributed text
Text(text[text.startIndex..<range.lowerBound]) +
//Append the attributed text
Text(text[range]).bold() +
//Search for additional instances of text that needs attribution
AttributedTextView(text: String(text[range.upperBound..<text.endIndex]))
} else {
//If the searched text is not found, add the rest of the string to the end
Text(text)
}
}
I get an error Cannot convert value of type 'some View' to expected argument type 'Text', with the recommended fix being to update the recursive line to AttributedTextView(text: String(text[range.upperBound..<text.endIndex])) as! Text. I apply this fix, but still see the same compiler error with the same suggested fix.
A few workarounds that I've tried:
Changing the return type from some View to Text. This creates a different error Cannot convert value of type '_ConditionalContent<Text, Text>' to specified type 'Text'. I didn't really explore this further, as it does make sense that the return value is reliant on that conditional.
Returning a Group rather than a Text, which causes additional errors throughout the SwiftUI file
Neither of these solutions feel very "Swifty". What is another way to go about this? Am I misunderstanding something in SwiftUI?
There are a few things to clarify here:
The + overload of Text only works between Texts which is why it's saying it cannot convert some View (your return type) to Text. Text + Text == Text, Text + some View == ☠️
Changing the return type to Text doesn't work for you because you're using #ViewBuilder, remove #ViewBuilder and it'll work fine.
Why? #ViewBuilder allows SwiftUI to defer evaluation of the closure until later but ensures it'll result in a specific view type (not AnyView). In the case where your closure returns either a Text or an Image this is handy but in your case where it always results in Text there's no need, #ViewBuilder forces the return type to be ConditionalContent<Text, Text> so that it could have different types.
Here's what should work:
private static func attributedTextView(text: String) -> Text {
if let range = text.range(of: "[0-9]+d[0-9]+", options: .regularExpression) {
//The unattributed text
return Text(text[text.startIndex..<range.lowerBound]) +
//Append the attributed text
Text(text[range]).bold() +
//Search for additional instances of text that needs attribution
AttributedTextView(text: String(text[range.upperBound..<text.endIndex]))
} else {
//If the searched text is not found, add the rest of the string to the end
return Text(text)
}
}
I made it static too because there's no state here it's a pure function and lowercased it so it was clear it was a function not a type (the function name looks like a View type).
You'd just call it Self.attributedTextView(text: ...)

How do I get the string value from Text?

I have a custom view that gets passed some Text objects. But I can't figure out how to get the string value out of them. For example:
let text = Text("hello")
// no way to get "hello" back
I've gone through the documentation, but for some reason the properties are not publicized.
in SwiftUI, you can use #State to bind variables you want to change.
#State var value = "Test value"
and in the view:
Text(value)
Then you can normally
print(value)
I'm not sure of the context and if this helps with your particular scenario, but I came across your post as I had just been having trouble testing a Text value.
What I worked out for me, was I could use the Text's verbatim parameter to test the String (as below):
it("should show the content I'm after") {
expect(viewModel.someText) == Text(verbatim: "some content I'm after")
}

SwiftUI - Localization of a dynamic Text

I am struggling with the locilization of some of my TextFields.
Usually a "normal" localization of a Text() or TextField() works without any problem in my App if the text I want to translate is hardcoded like this:
Text("English Text")
I translate it in my Localizable.strings like this:
"English Text" = "German Text";
Now I want to translate TextFields which are more dynamic, but where I know each possible entry:
TextField("New note" + (refresh ? "" : " "),text: $newToDo, onCommit: {
self.addToDo()
self.refresh.toggle()
})
(The refresh is necessary because of a SwiftUI bug sometimes not showing the placeholder-text again.)
Another example would be:
func dayWord() -> String {
let dateFormatter = DateFormatter()
dateFormatter.timeZone = TimeZone.current
dateFormatter.locale = Locale(identifier: "de_DE")
dateFormatter.dateFormat = "EEEE"
return dateFormatter.string(from: self)
}
var day: String {
return data.date.dateFromMilliseconds().dayWord()
}
Text(day.prefix(2))
The Text(day.prefix(2)) has only seven possible states, but I don't know what to write as a key in my Localizable.strings.
Use NSLocalizedString, like
TextField(NSLocalizedString("New note", comment: "") + (refresh ? "" : " "), ...
According to SwiftUI convention, the Text() label is automatically localized only when it has a string "literal". When the text is a variable, you need to use LocalizedStringKey(). Other localizable texts inside Button() or TextField() also require LocalizedStringKey(). So you need to change this to:
Text(LocalizedStringKey(String(day.prefix(2))))
This converts day.prefix(2) into a String, because it is actually a substring, and then calls LocalizedStringKey() to make it localizable. In your Localizable.strings file you could add all 7 possibilities.
"Mo" = "Mo";
"Di" = "Tu";
//etc.
but why would you? Instead, use:
dateFormatter.locale = Locale(identifier: Locale.preferredLanguages.first)
...
Text(day.prefix(2))
to determine the user's current language and display that. Apple returns the text in the proper language, so this text doesn't need to be localized any further.
TextField() does need localization using LocalizedStringKey():
TextField(LocalizedStringKey("New note") + (refresh ? "" : " "),text: $newToDo, onCommit: {
self.addToDo()
self.refresh.toggle()
})
As Asperi points out, for "New note" the same can be accomplished using NSLocalizedString(), which might be better depending on how you like to work. The benefits are: easily adding a comment for the translator, and automatic export into the xliff when you choose Editor -> Export for localization…
By contrast, SwiftUI's LocalizedStringKey() requires you to manually add strings to the Localizable.strings file. For your day.prefix(2) example, I think it would make more sense to get the user's preferred language and display the localized date directly.
SwiftUI Text will only localize literal strings, strings defined in double quotes. You have to either create localized key or retrieve localized string.
Examples
Simple localization with string literal key.
Text("Label")
If you construct the label, you can use LocalizedStringKey function to localize your computed label.
let key = "Label"
let localizedKey = LocalizedStringKey(key)
Text(localizedKey)
You can also get the localized string using NSLocalizedString.
let localizedString = NSLocalizedString("Label", comment: "")
Text(localizedString)
It's also possible to have localized strings accept arguments (#, lld, and lf) through String Interpolation. For example you could have the following localizations in your project Localizable.strings file:
"Name %#" = "Name %#";
"Number %lld" = "%lld is the number";
And you can use it like this:
Text("Label \(object)")
Text("Number \(number)")
Xcode Search
If you want to search for calls of Text that aren't done with literal strings in your projects. You can use Xcode Find Regular Expression feature. View > Navigators > Find or Cmd-4 keyboard shortcut. Change the search type just above the search field to Regular Expression.
Use the following regular expression: Text\([^\"]

SwiftUI TextField won't change its contents under weird corner case with Binding<String> computed property

I have a TextField inside a SwiftUI body. I have bound it to a #State var through an intermediary binding which lets me get and set a computed value...
struct ExampleView: View {
#State var symbolToBeValidated: String = ""
var body: some View {
let binding = Binding<String> (get: {
return self.symbolToBeValidated
}, set: {
var newString = $0.uppercased()
self.symbolToBeValidated = newString // <- fig. 1: redundant assignment I wish I didn't have to put
newString = newString.replacingOccurrences(
of: #"[^A-Z]"#,
with: "",
options: .regularExpression
)
self.symbolToBeValidated = newString // <- fig. 2: the final, truly valid assignment
})
let form = Form {
Text("What symbol do you wish to analyze?")
TextField("ex.AAPL", text: binding)
// [...]
I'm using the intermediary Binding so that I can transform the string to always be an Uppercased format only containing letters A-Z (as referenced by my .regularExpression). (I'm trying to make it so that the TextField only shows a validly formatted Stock Symbol on each keypress).
This works, somewhat. The problem I discovered is that if I don't call the assignment twice (as seen in fig 1) the TextField will begin to show numbers and letters (even though it isn't included in the symbolToBeValidated string. This happens, I suspect, because SwiftUI is checking the oldValue against the newValue internally, and because it hasn't changed in the background, it doesn't call a refresh to get the internal value again. The way I've found to thwart this is to include an extra assignment before the .replacingOccurences call.
This results in the number or symbol being flashed on the screen for a blip as it is being typed by the user, then it is correctly removed by the .replacingOccurences call.
There must be a more elegant way to do this. I went down the Formatter class type and tried this alternative only because Formatter resulted in a similar behavior where the errant character would blip on the screen before being removed.
If someone knows a way for this to be intercepted before anything is displayed on the screen, I would appreciate it. This is super nit-picky, but I'm just fishing for the right answer here.
Try this:
extension Binding where Value == String {
public func validated() -> Self {
return .init(
get: { self.wrappedValue },
set: {
var newString = $0.uppercased()
newString = newString.replacingOccurrences(
of: #"[^A-Z]"#,
with: "",
options: .regularExpression
)
self.wrappedValue = newString
}
)
}
}
// ...
TextField("ex.AAPL", text: self.$symbolToBeValidated.validated())
This way also allows you to reuse and test the validation code.

Currency input in SwiftUI TextField

I am trying to implement the ability to change a currency value in an edit mode detailed view, but I am struggling to get formatted input for numeric values working.
struct JobDetailView_Edit: View {
#State var jobDetails: JobDetails
var currencyFormatter: NumberFormatter = {
let f = NumberFormatter()
f.numberStyle = .currency
return f
}()
var body: some View {
Form {
Section(header: Text("General")) {
HStack {
Text("Job Name")
Spacer()
TextField($jobDetails.jobName)
.multilineTextAlignment(.trailing)
}
TextField($jobDetails.hourlyRateBasic, formatter: currencyFormatter)
}
... other unimportant code...
The data is passed in correctly, and the textfield displays the formatted value which is stored in $jobDetails.hourlyRateBasic, however, I cannot edit the field as I would be able to if it were a string. If I try to edit the field then press enter in the simulator, I get the following error message:
[SwiftUI] Failure setting binding's value. The supplied formatter does not produce values of type Double. This may be resolved by ensuring the binding and the output of the formatter are of the same type.
FYI $jobDetails.hourlyRateBasic is of type double.
I have created a component that wraps around a UITextfield.
You can check it out here https://github.com/youjinp/SwiftUIKit
Here's the demo
Ben Scheirman created a Tip Calculator that used a TextField with a currency formatter. The difference between his code and yours is that he stored the currency value in a Double?. NumberFormatter returns a NSNumber?.
You can see his code at https://github.com/nsscreencast/397-swiftui-tip-calculator