How to get the language code back from a localizedString? - swift

I have a Picker in my Settings tab that displays all the supported languages to the user. Showing the language codes is not very human-readable, so I display the localizedStrings in the Picker instead. I need to retrieve the original language code back from this localizedString, however, to store it in the UserDefault (or in this case, through the #AppStorage). Is there a way to do this through Locale (or any other built-in library)? I read the documentation and tried looking for similar questions on StackOverflow / the Apple developer forum, but all questions seem to be about language code --> localizedString, rather than localizedString -> language code. Alternatively, I can also make an enum that stores all this information for me, but I'd like to know whether there's a better way of doing this.
The code:
#AppStorage("language") var language: String = "en"
#State var selectedLanguage = "English"
Picker("settingsTabGeneralSectionHeader".localized(), selection: $selectedLanguage) {
ForEach(Bundle.main.localizations, id: \.self) {
Text((Locale.current as NSLocale).localizedString(forLanguageCode: $0)!)
}
}
.onChange(of: selectedLanguage) { selection in
language = ??? // Inverse of Locale.localizedString
}

We can bind Picker selection directly to AppStorage and use code as tag to match, so code is simplified to
struct ContentView: View {
#AppStorage("language") var language: String = "en"
var body: some View {
VStack {
Text("Selected: " + language)
Picker("", selection: $language) {
ForEach(Bundle.main.localizations, id: \.self) {
Text((Locale.current as NSLocale)
.localizedString(forLanguageCode: $0)!).tag($0) // << here !!
}
}
}
}
}
and selection separated from presentation.
Tested with Xcode 13.4 / iOS 15.5
Test module on GitHub

Related

Why does Swift's TextField take in a title if it's not used?

I noticed the first argument of TextField takes in a "title", but to me, this string is never used. I don't see it displayed on the simulator.
I suspected maybe it is just as a unique ID for that TextField, but it's not because I duplicated it and it build. Plus the "title" name doesn't really match that description.
I was wondering what the purpose of the parameter was or how to access it for display? Thanks!
struct ContentView: View {
#State private var checkAmount = 0
var body: some View {
NavigationView {
Form {
Section {
TextField("Check amount", value: $checkAmount, format: .currency(code: Locale.current.currencyCode ?? "USD"))
}
Section {
Text("Input")
}
} .navigationTitle("Tabshare")
}
}
}
Please don't forget about accessibility - specifically with screen readers and Voice Control. Adding a description of a text field is crucial for people that use these features.
Don't forget that SwiftUI is cross-platform — macOS heavily utilises Text Field titles.

How to translate / localize a variable in Swift

I have this code and I have localization setup successfully. I created a Categories.strings file and localized it, but the translation of the categories is not working.
Code:
struct CategoriesViewRow: View {
var selection: SimpleJson
var body: some View {
HStack {
Image(selection.name)
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
Text(String(localized: "\(selection.name)", table: "Categories", comment: "Category"))
Spacer()
}
}
}
This var selection: SimpleJson is a struct that looks like this:
struct SimpleJson: Hashable, Codable, Identifiable {
var id: Int
var name: String
var description: String?
var hidden: Bool?
}
It contains some static categories loaded from a file. They need to be loaded from the file, and I always managed to translate them in Objective-C. But now in Swift, it seems like I am missing something.
Normal SwiftUI Text() code gets translated correctly, but with my other code, there is something wrong.
I am currently using Xcode Beta 13.0, but I am quite sure this isn't the problem. I rather think I am doing something wrong, but I can't seem to find some good info for my use-case.
Can someone help me out?
Thanks in advance
You can add this function to a String extension and then call .localize() on string variables:
extension String {
/**
So that when you return a String it also adjusts the language
- Parameter comment: To describe specific meaning if it's unclear (e.g.: bear could mean the animal or the verb to bear)
- Returns: The localized String duh
- Usage: str.localize()
- Further reading: https://www.hackingwithswift.com/example-code/uikit/how-to-localize-your-ios-app
*/
func localize(comment: String = "") -> String {
NSLocalizedString(self, comment: comment)
}
}

What's best practice for programmatic movement a NavigationView in SwiftUI

I'm working on an app that needs to open on the users last used view even if the app is completly killed by the user or ios.
As a result I'm holding last view used in UserDefaults and automatically moving the user through each view in the stack until they reach their destination.
The code on each view is as follows:
#Binding var redirectionID: Int
VStack() {
List {
NavigationLink(destination: testView(data: data, moc: moc), tag: data.id, selection:
$redirectionId) {
DataRow(data: data)
}
}
}.onAppear() {
redirectionID = userData.lastActiveView
}
Is there a better / standard way to achieve this? This works reasonably on iOS 14.* but doesn't work very well on iOS 13.* On iOS 13.* The redirection regularly doesnt reach its destination page and non of the preceeding views in the stack seem to be created. Pressing back etc results in a crash.
Any help / advice would be greatly appreciated.
This sounds like the perfect use of if SceneStorage
"You use SceneStorage when you need automatic state restoration of the value. SceneStorage works very similar to State, except its initial value is restored by the system if it was previously saved, and the value is· shared with other SceneStorage variables in the same scene."
#SceneStorage("ContentView.selectedProduct") private var selectedProduct: String?
#SceneStorage("DetailView.selectedTab") private var selectedTab = Tabs.detail
It is only available in iOS 14+ though so something manual would have to be implemented. Maybe something in CoreData. An object that would have variables for each important state variable. It would work like an ObservedObject ViewModel with persistence.
Also. you can try...
"An NSUserActivity object captures the app’s state at the current moment in time. For example, include information about the data the app is currently displaying. The system saves the provided object and returns it to the app the next time it launches. The sample creates a new NSUserActivity object when the user closes the app or the app enters the background."
Here is some sample code that summarizes how to bring it all together. It isn't a minimum reproducible example because it is a part of the larger project called "Restoring Your App's State with SwiftUI" from Apple. But it gives a pretty good picture on how to implement it.
struct ContentView: View {
// The data model for storing all the products.
#EnvironmentObject var productsModel: ProductsModel
// Used for detecting when this scene is backgrounded and isn't currently visible.
#Environment(\.scenePhase) private var scenePhase
// The currently selected product, if any.
#SceneStorage("ContentView.selectedProduct") private var selectedProduct: String?
let columns = Array(repeating: GridItem(.adaptive(minimum: 94, maximum: 120)), count: 3)
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(productsModel.products) { product in
NavigationLink(destination: DetailView(product: product, selectedProductID: $selectedProduct),
tag: product.id.uuidString,
selection: $selectedProduct) {
StackItemView(itemName: product.name, imageName: product.imageName)
}
.padding(8)
.buttonStyle(PlainButtonStyle())
.onDrag {
/** Register the product user activity as part of the drag provider which
will create a new scene when dropped to the left or right of the iPad screen.
*/
let userActivity = NSUserActivity(activityType: DetailView.productUserActivityType)
let localizedString = NSLocalizedString("DroppedProductTitle", comment: "Activity title with product name")
userActivity.title = String(format: localizedString, product.name)
userActivity.targetContentIdentifier = product.id.uuidString
try? userActivity.setTypedPayload(product)
return NSItemProvider(object: userActivity)
}
}
}
.padding()
}
.navigationTitle("ProductsTitle")
}
.navigationViewStyle(StackNavigationViewStyle())
.onContinueUserActivity(DetailView.productUserActivityType) { userActivity in
if let product = try? userActivity.typedPayload(Product.self) {
selectedProduct = product.id.uuidString
}
}
.onChange(of: scenePhase) { newScenePhase in
if newScenePhase == .background {
// Make sure to save any unsaved changes to the products model.
productsModel.save()
}
}
}
}

SwiftUI TextField behaves weirdly when I bind its value (can't type Chinese, occasionally backspaces skip a character)

Here's the code a minimal repro. I suspect its something to do with changing the SearchTerm causing the UI to rerender or something, but I can't quite figure this out. Ideally, I'd onEditingChange but I want to react to every individual keypress.
struct ContentView: View {
#State var searchTerm: String = "Hello World!"
var body: some View {
let binding = Binding<String>(get: {
self.searchTerm
}, set: {
if $0 != self.searchTerm
{
self.searchTerm = $0
}
})
return VStack{
TextField("Search field", text: binding)
Text(searchTerm)
}
}
}
Here you can see a gif of the buggy behavior
(this is on the latest non-beta Xcode, Swift, macOS, etc.)
Edit: I think this might just be a SwiftUI bug - https://developer.apple.com/forums/thread/128721
SwiftUI TextField keyboard flashes and breaks typing in other languages
https://developer.apple.com/forums/thread/128721
Hi everyone else who also encounters this issue. I believe this is a platform bug that can't be worked around, and my solution was simply to use a wrapped UIKit UITextField... which is what I was hoping to avoid.

How to set canvas preview language?

(currently trying on Xcode 11 Beta 7)
I want to pass an already localized string to Text() and see how it looks on canvas using ".environment(.locale, .init(identifier:"ja"))", but the preview is always set to whatever language that I have set on the scheme settings.
I know that it works if I pass a LocalizedStringKey directly, like Text("introTitle"), but I do not want to do that. Instead I want to use enums, like Text(L10n.Intro.title), but when I do that the environment operator is overriden by the scheme settings language.
Is this a bug or expected behaviour?
struct ContentView: View {
var body: some View {
Text("introTitle") //this works
Text(L10n.Intro.title) //this doesn't
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ForEach(["en", "ja", "pt"], id: \.self) { localeIdentifier in
ContentView()
.environment(\.locale, .init(identifier: localeIdentifier)) //this gets ignored, and only the scheme settings language is previewed
.previewDisplayName(localeIdentifier)
}
}
}
internal enum L10n {
internal enum Intro {
internal static let title = NSLocalizedString("introTitle", comment: "")
internal static let title2 = "introTitle" //this also doesn't work
}
}
In Localizable.strings I have:
//english
"introTitle" = "Welcome!";
//japanese
"introTitle" = "ようこそ!";
//portuguese
"introTitle" = "Bem-vindo(a)!";
My preferred approach would be to use Text("introTitle"), but if you want to use enums for your localised keys, you have to declare it like this
internal enum L10n {
internal enum Intro {
internal static let title = LocalizedStringKey("introTitle")
}
}
and then you will be able to use it like this:
Text(L10n.Intro.title)
In your code title is of type NSLocalisedString, title2 is a String, but you need a LocalizedStringKey to pass into Text initialiser.