Change App Localization Based on User Selection - ios

I've read lots of answers about this, but most are many years old and I don't know what's the latest info.
On first launch, my app will ask the user which language (s)he prefers, rather than just using the OS default language.
How can I set the localization rest of the app to the selected language?
How can I get which language the user has selected in other views?
I thought the following code would set the language, but it didn't do the job:
UserDefaults.standard.set("AR", forKey: "AppleLanguages")
UserDefaults.standard.synchronize()

I have handled this with a LanguageManager singleton class, which handles all the localization. Here is some psuedo code, as its typically a fairly large class.
It has a list of all locales that are supported, for example:
let supportedLocales = ['en-US', 'en-CA', 'fr', 'es-ES', 'es-MX']
It also stores the selected language in UserDefaults. When the selectedLocale changes, it sends a Notification, in case you want to notify your views or anything else:
var selectedLocale: String? {
get {
return UserDefaults.standard.object(forKey: UserDefaultsKeys.selectedLocale) as? String
}
set (newLocale) {
let didChange = self.selectedLocale != newLocale
UserDefaults.standard.set(newLocale, forKey: UserDefaultsKeys.selectedLocale)
UserDefaults.standard.synchronize()
if didChange {
NotificationCenter.default.post(name: Notification.Name.localeDidChange, object: nil)
}
}
}
Now, in order to pull strings out of your localized.strings files, you can't use the standard Apple methods - you have to provide your own. Use LanguageManager.shared.getString(for key:String, alt:String) to reference keys in your .strings file to pull out localized strings.
var selectedLanguage:String? {
//returns just the language portion of the locale - eg: 'en' from 'en-US'
if let selectedLocale = selectedLocale {
return selectedLocale.components(separatedBy: "-")[0]
}
return nil
}
func getString(for key:String, alt:String) -> String
{
var val:String? = getString(for:key, language: selectedLocale)
if val == nil {
val = getString(for:key, language: selectedLanguage)
}
if val == nil {
val = getString(for:key, language: "en") //default to English
}
if let val = val {
return val
}
return alternate //use fallback
}
func getString(for key:String, language:String) -> String?
{
let path = Bundle.main.path(forResource:language, ofType:"lproj")
if let languageBundle = Bundle(path:path) {
return languageBundle.localizedString(for: key)
}
return nil
}

By default is the English Language // You can set Language according you
UserDefaults.standard.set("en", forKey: "Apple")
UserDefaults.standard.synchronize()
If you want to current system language than use code
let langStr = Locale.current.languageCode //and set in UserDeafaults
UserDefaults.standard.set(langStr, forKey: "Apple")
var currentlanguage: String?
self.currentlanguage = UserDefaults.standard.object(forKey: "Apple") as! String?
print("current language ---%#",self.currentlanguage)

Related

How to reload content text after change language iOS Swift code

I have a problem when I change the application language using swift code. In my case I had to use the xliff file that was automatically generated from the storyboard/xib.
My code:
let APPLE_LANGUAGE_KEY = "AppleLanguages"
/// L102Language
class L102Language {
/// get current Apple language
class func currentAppleLanguage() -> String{
let userdef = UserDefaults.standard
let langArray = userdef.object(forKey: APPLE_LANGUAGE_KEY) as! NSArray
let current = langArray.firstObject as! String
return current
}
/// set #lang to be the first in Applelanguages list
class func setAppleLAnguageTo(lang: String) {
let userdef = UserDefaults.standard
userdef.set([lang,currentAppleLanguage()], forKey: APPLE_LANGUAGE_KEY)
userdef.synchronize()
}
}
Use:
if L102Language.currentAppleLanguage() == "en" {
L102Language.setAppleLAnguageTo(lang: "vi")
UIView.appearance().semanticContentAttribute = .forceRightToLeft
} else {
L102Language.setAppleLAnguageTo(lang: "en")
UIView.appearance().semanticContentAttribute = .forceLeftToRight
}
After userdef.synchronize() is executed the application does not change the language. It only really works when I restart the app. I think this way is not good. In this case, what else do I need to do to change the language of the application without restarting.
thanks everyone
Update:
I resolved problem with answer https://stackoverflow.com/a/48187049/12429634
Thanks everyone!
You will need to register for the NSLocale currentLocaleDidChangeNotification, and write code to update your UI when you get notified.

How to design a Settings model to generically get + set values for all UserDefaults keys

I’m setting up my Settings class which gets/sets values from UserDefaults. I wish for as much of the code to be generic to minimise effort involved whenever a new setting is introduced (I have many as it is, and I expect many more in the future too), thereby reducing the probability of any human errors/bugs.
I came across this answer to create a wrapper for UserDefaults:
struct UserDefaultsManager {
static var userDefaults: UserDefaults = .standard
static func set<T>(_ value: T, forKey: String) where T: Encodable {
if let encoded = try? JSONEncoder().encode(value) {
userDefaults.set(encoded, forKey: forKey)
}
}
static func get<T>(forKey: String) -> T? where T: Decodable {
guard let data = userDefaults.value(forKey: forKey) as? Data,
let decodedData = try? JSONDecoder().decode(T.self, from: data)
else { return nil }
return decodedData
}
}
I’ve created an enum to store all the setting keys:
enum SettingKeys: String {
case TimeFormat = "TimeFormat"
// and many more
}
And each setting has their own enum:
enum TimeFormat: String, Codable {
case ampm = "12"
case mili = "24"
}
In this simplified Settings class example, you can see that when it’s instantiated I initialise the value of every setting defined. I check if its setting key was found in UserDefaults: if yes, I use the value found, otherwise I set it a default and save it for the first time to UserDefaults.
class Settings {
var timeFormat: TimeFormat!
init() {
self.initTimeFormat()
}
func initTimeFormat() {
guard let format: TimeFormat = UserDefaultsManager.get(forKey: SettingKeys.TimeFormat.rawValue) else {
self.setTimeFormat(to: .ampm)
return
}
self.timeFormat = format
}
func setTimeFormat(to format: TimeFormat) {
UserDefaultsManager.set(format, forKey: SettingKeys.TimeFormat.rawValue)
self.timeFormat = format
}
}
This is working fine, and pretty straightforward. However, thinking ahead, this will be tedious (and therefore error-prone) to replicate for every setting in this app (and every other I look to do in the future). Is there a way for the init<name of setting>() and set<name of setting>() to be generalised, whereby I pass it a key for a setting (and nothing more), and it handles everything else?
I’ve identified every setting to have these shared elements:
settings key (e.g. SettingsKey.TimeFormat in my example)
unique type (could be AnyObject, String, Int, Bool etc. e.g. enum TimeFormat in my example)
unique property (e.g. timeFormat in my example)
default value (e.g. TimeFormat.ampm in my example)
Thanks as always!
UPDATE:
This may or may not make a difference, but considering all settings will have a default value, I realised they don’t need to be a non-optional optional but can have the default set at initialisation.
That is, change:
var timeFormat: TimeFormat!
func initTimeFormat() {
guard let format: TimeFormat = UserDefaultsManager.get(forKey: SettingKeys.TimeFormat.rawValue) else {
self.setTimeFormat(to: .ampm)
return
}
self.timeFormat = format
}
To:
var timeFormat: TimeFormat = .ampm
func initTimeFormat() {
guard let format: TimeFormat = UserDefaultsManager.get(forKey: SettingKeys.TimeFormat.rawValue) else {
UserDefaultsManager.set(self.timeFormat, forKey: SettingKeys.TimeFormat.rawValue)
return
}
self.timeFormat = format
}
The setTimeFormat() function is still needed when its value is changed by the user in-app.
Note that your settings don't have to be stored properties. They can be computed properties:
var timeFormat: TimeFormat {
get {
UserDefaultsManager.get(forKey: SettingKeys.TimeFormat.rawValue) ?? .ampm
}
set {
UserDefaultsManager.set(newValue, forKey: SettingKeys.TimeFormat.rawValue)
}
}
You don't have to write as much code this way, when you want to add a new setting. Since you will just be adding a new setting key enum case, and a computed property.
Further more, this computed property can be wrapped into a property wrapper:
#propertyWrapper
struct FromUserDefaults<T: Codable> {
let key: SettingKeys
let defaultValue: T
init(key: SettingKeys, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
UserDefaultsManager.get(forKey: key.rawValue) ?? defaultValue
}
set {
UserDefaultsManager.set(newValue, forKey: key.rawValue)
}
}
}
Usage:
class Settings {
#FromUserDefaults(key: .TimeFormat, defaultValue: .ampm)
var timeFormat: TimeFormat
}
Now to add a new setting, you just need to write a new enum case for the key, and two more lines of code.

Localization request permission base on the application language not device language

Localization is working fine in my application. I want localization the permission dialog text messages. It is working fine with the device language changes but I want to change message according to my application language.
I have tried the following code
import UIKit
class LocalizeHelper: NSObject {
private var myBundle: Bundle? = nil
static let shared: LocalizeHelper = {
let instance = LocalizeHelper()
return instance
}()
override init() {
super.init()
// use systems main bundle as default bundle
myBundle = Bundle.main
}
func localizedString(forKey key: String) -> String {
return myBundle!.localizedString(forKey: key, value: "", table: nil)
}
// Converted with Swiftify v1.0.6331 - https://objectivec2swift.com/
func setLanguage(_ lang: String) {
// path to this languages bundle
let path: String? = Bundle.main.path(forResource: lang, ofType: "lproj")
if path == nil {
// there is no bundle for that language
// use main bundle instead
myBundle = Bundle.main
}
else {
// use this bundle as my bundle from now on:
myBundle = Bundle(path: path!)
// to be absolutely shure (this is probably unnecessary):
if myBundle == nil {
myBundle = Bundle.main
}
}
}
func getLanguage() -> String {
print("\(String(describing: myBundle?.bundlePath.last))")
//return myBundle!.bundlePath.last >> Error
return myBundle!.bundlePath.lastCharacter!
}
}
extension String {
public var lastCharacter: String? {
guard let aLast = self.last else {
return nil
}
return String(aLast)
}
}
I have surfed in StackOverflow but didn't find any solution. Any help shell we appreciated.
Your best bet is to save the application's language in the user defaults as a string for example and then when you take it out of user defaults parse it as a custom app language enum. If you don't have any app language save you can always fall through to the device language but prefer or override it with the app language in user defaults not being nil. Also preferably you should wrap your user defaults interactions with a user defaults manager that you access in your view controller.
So the suggested steps are:
create userdefualts manager
define language enum
implement setLanguage func language parameter
implement getLanguage func language output with device language or
english as guard return

How to store user data as string, and load that string in viewDidLoad

I am trying to load a value that has been inputted by the user in the viewDidLoad via a String. I am using UserDefaults to save the users value that they input into a UITextField (userValue), I then save this to the String 'search'. I am able to print out the value of search in the GoButton function, and it works fine, but when I load my ViewController as new, the value of 'search' is equal to nil. The aim here is to have the users previous search saved, and loaded into the UITextField (that is used as a search box) upon loading the ViewController.
Code Below:
class ViewController: UIViewController {
#IBOutlet weak var userValue: UITextField!
var search: String!
}
viewDidLoad:
override func viewDidLoad() {
if (search != nil)
{
userValue.text! = String (search)
}
}
Button Function:
#IBAction func GoButton(_ sender: Any) {
let userSearch: String = userValue.text!
let perference = UserDefaults.standard
perference.set(userSearch, forKey: "hello")
perference.value(forKey: "hello")
let value = perference.value(forKey: "hello") as! String
search = value
print (search) // <<this works, it prints out the users search value
}
#VishalSharma has the right idea, but the code should probably look more like…
override func viewDidLoad() {
super.viewDidLoad()
if let search = UserDefaults.standard.string(forKey: "hello") {
userValue.text = search
}
}
or even more simply…
userValue.text = UserDefaults.standard.string(forKey: "hello")
When you load, search is effectively nil.
So either you read userDefaults in viewDidload or you come through a segue: then you can load search in the prepare.
I've always found it convenient and useful to store all UserDefault properties as an extension within the same file along with their getters and setters. It is far easier to maintain, use and read. by using the #function keyword for the key you are referencing the variable's name and not a string that can be accidentally changed somewhere else in code.
UserDefaults.swift
import Foundation
// An Extension to consolidate and manage user defaults.
extension UserDefaults {
/// A value Indicating if the user has finished account setup.
/// - Returns: Bool
var finishedAcountSetup: Bool {
get { return bool(forKey: #function) }
set { set(newValue, forKey: #function) }
}
/// The hello text at the start of the application.
/// - Returns: String?
var helloText: String? {
get { return string(forKey: #function) }
set {set(newValue, forKey: #function) }
}
//etc...
}
When you use these values reference the standard settings:
//Setting
UserDefaults.standard.helloText = "Updated Hello Text"
// Getting
// for non-optional value you can just get:
let didCompleteSetup = UserDefaults.standard.finishedAcountSetup
// Otherwise, safely unwrap the value with `if-let-else` so you can set a default value.
if let text = UserDefaults.standard.helloText {
// Ensure there is text to set, otherwise use the default
label.text = text
} else {
// helloText is nil, set the default
label.text = "Some Default Value"
}
obviously, it provides nil because when view controller load the search is nil try this.
let perference = UserDefaults.standard
override func viewDidLoad() {
super.viewDidLoad()
if (perference.value(forKey: "hello") != nil) {
search = perference.value(forKey: "hello") as! String
userValue.text! = String (search)
}
}

Is there a better way to save a custom class to NSUserDefaults than encoding and decoding everything with NSCoder?

My current class has around 50 lines just encoding and decoding variables in order for my class to be NSUserDefaults compatible. Is there a better way to handle this?
Example:
init(coder aDecoder: NSCoder!) {
lightEnabled = aDecoder.decodeBoolForKey("lightEnabled")
soundEnabled = aDecoder.decodeBoolForKey("soundEnabled")
vibrateEnabled = aDecoder.decodeBoolForKey("vibrateEnabled")
pulseEnabled = aDecoder.decodeBoolForKey("pulseEnabled")
songs = aDecoder.decodeObjectForKey("songs") as! [Song]
currentSong = aDecoder.decodeIntegerForKey("currentSong")
enableBackgroundSound = aDecoder.decodeBoolForKey("enableBackgroundSound")
mixSound = aDecoder.decodeBoolForKey("mixSound")
playSoundInBackground = aDecoder.decodeBoolForKey("playSoundInBackground")
duckSounds = aDecoder.decodeBoolForKey("duckSounds")
BPMBackground = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("BPMBackgorund") as! NSData) as! UIColor!
BPMPulseColor = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("BPMPulseColor") as! NSData) as! UIColor!
TempoBackGround = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("TempoBackGround") as! NSData) as! UIColor!
TempoPulseColor = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("TempoPulseColor") as! NSData) as! UIColor!
TimeBackGround = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("TimeBackGround") as! NSData) as! UIColor!
TimeStrokeColor = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("TimeStrokeColor") as! NSData) as! UIColor!
TextColor = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("TextColor") as! NSData) as! UIColor!
}
func encodeWithCoder(aCoder: NSCoder!) {
aCoder.encodeBool(lightEnabled, forKey: "lightEnabled")
aCoder.encodeBool(soundEnabled, forKey: "soundEnabled")
aCoder.encodeBool(vibrateEnabled, forKey: "vibrateEnabled")
aCoder.encodeBool(pulseEnabled, forKey: "pulseEnabled")
aCoder.encodeObject(songs, forKey: "songs")
aCoder.encodeInteger(currentSong, forKey: "currentSong")
aCoder.encodeBool(enableBackgroundSound, forKey: "enableBackgroundSound")
aCoder.encodeBool(mixSound, forKey: "mixSound")
aCoder.encodeBool(playSoundInBackground, forKey: "playSoundInBackground")
aCoder.encodeBool(duckSounds, forKey: "duckSounds")
aCoder.encodeObject(BPMBackground.archivedData(), forKey: "BPMBackground")
aCoder.encodeObject(BPMPulseColor.archivedData(), forKey: "BPMPulseColor")
aCoder.encodeObject(TempoBackGround.archivedData(), forKey: "TempoBackGround")
aCoder.encodeObject(TempoPulseColor.archivedData(), forKey: "TempoPulseColor")
aCoder.encodeObject(TimeBackGround.archivedData(), forKey: "TimeBackGround")
aCoder.encodeObject(TimeStrokeColor.archivedData(), forKey: "TimeStrokeColor")
aCoder.encodeObject(TextColor.archivedData(), forKey: "TextColor")
}
You should create a struct or enum to organise your keys, because your way is just prone to typos. Just put it right above your class
enum Key: String {
case allSettings
case lightEnabled
case soundEnabled
}
and than just call the keys like so
...forKey: Key.lightEnabled.rawValue)
Now in regards to your question, I was facing the same issue with my game trying to save properties for 40 levels (bestTimes, Level unlock status etc). I initially did what you tried and it was pure madness.
I ended up using arrays/dictionaries or even arrays of dictionaries for my data which cut down my code by like 80 percent.
Whats also nice about this is that say you need to save something like LevelUnlock bools, it will make your life so much easier later on. In my case I have a UnlockAllLevels button, and now I can just loop trough my dictionary/array and update/check the levelUnlock bools in a few lines of code. So much better than having massive if-else or switch statements to check each property individually.
For example
var settingsDict = [
Key.lightEnabled.rawValue: false,
Key.soundEnabled.rawValue: false,
...
]
Than in the decoder method you say this
Note: This way will take into account that you might add new values to the SettingsDict and than on the next app launch those values will not be removed because you are not replacing the whole dictionary with the saved one, you only update the values that already exist.
// If no saved data found do nothing
if var savedSettingsDict = decoder.decodeObjectForKey(Key.allSettings.rawValue) as? [String: Bool] {
// Update the dictionary values with the previously saved values
savedSettingsDict.forEach {
// If the key does not exist anymore remove it from saved data.
guard settingsDict.keys.contains($0) else {
savedSettingsDict.removeValue(forKey: $0)
return
}
settingsDict[$0] = $1
}
}
If you use multiple dictionaries than your decoder method will become a messy again and you will also repeat alot of code. To avoid this you can create an extension of NSCoder using generics.
extension NSCoder {
func decodeObject<T>(_ object: [String: T], forKey key: String) -> [String: T] {
guard var savedData = decodeObject(forKey: key) as? [String: T] else { return object }
var newData = object
savedData.forEach {
guard object.keys.contains($0) else {
savedData[$0] = nil
return
}
newData[$0] = $1
}
return newData
}
}
and than you can write this in the decoder method for each dictionary.
settingsDict = aDecoder.decodeObject(settingsDict, forKey: Key.allSettings.rawValue)
Your encoder method would look like this.
encoder.encodeObject(settingsDict, forKey: Key.allSettings.rawValue)
In your game/app you you can use them like so
settingsDict[Key.lightEnabled.rawValue] = true
if settingsDict[Key.lightEnabled.rawValue] == true {
/// light is turned on, do something
}
This way makes it also very easy to integrate iCloud KeyValue storage (just create an iCloud dictionary), again mainly because its so easy to save and compare a lot of values with very little code.
UPDATE:
To make calling these a bit easier I like to create some convenience getters/setters in the GameData class. This has the benefit that you can more easily call these properties in your project (like your old way) but your encode/decode method will still stay compact. You can also still do things such as looping to compare values.
var isLightEnabled: Bool {
get { return settingsDict[Key.lightEnabled.rawValue] ?? false }
set { settingsDict[Key.lightEnabled.rawValue] = newValue }
}
var isSoundEnabled: Bool {
get { return settingsDict[Key.soundEnabled.rawValue] ?? false }
set { settingsDict[Key.soundEnabled.rawValue] = newValue }
}
and than you can call them like normal properties.
isLightEnabled = true
if isLightEnabled {
/// light is turned on, do something
}
Look at protocol codeable in Swift 4.
The decoder and encoder will be auto-generated for you.
Check out: (starting about half way through)
https://developer.apple.com/videos/play/wwdc2017/212/

Resources