Swift 4 Settings Bundle, get defaults - ios

I created a settings bundle with about 8 toggle switches. What I am trying to do it get the default values from the settings bundle. Currently right now I have these two methods:
func registerSettingsBundle(){
let appDefaults = [String:AnyObject]()
UserDefaults.standard.register(defaults: appDefaults)
UserDefaults.standard.synchronize()
}
func updateDisplayFromDefaults(){
let defaults = UserDefaults.standard
let update_lot = defaults.bool(forKey: "update_lot")
print(update_lot)
}
and I am calling these methods in my viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
registerSettingsBundle()
updateDisplayFromDefaults()
}
However this does not get me the default values (which are all true, but they all return false). This works and gives me the correct values if I close down the app, open settings, adjust the settings and re-open the app. Is there away of getting the default settings? I went the route of reading the plist, but if I change the settings in my settings bundle, it would not take effect.

For the sake of the demonstration let's assume that you have two switches in the settings bundle. One with the default value set to YES and one with the default value set to NO.
If you want to be able to access default values defined in the Settings.bundle from the UserDefaults in your app you have to register them first. Unfortunately, iOS won't do it for you and you have to take care of it by yourself.
The following method scans the Root.plist associated with the Settings.bundle and registers the default values for the identifiers of your preferences.
func registerDefaultsFromSettingsBundle()
{
let settingsUrl = Bundle.main.url(forResource: "Settings", withExtension: "bundle")!.appendingPathComponent("Root.plist")
let settingsPlist = NSDictionary(contentsOf:settingsUrl)!
let preferences = settingsPlist["PreferenceSpecifiers"] as! [NSDictionary]
var defaultsToRegister = Dictionary<String, Any>()
for preference in preferences {
guard let key = preference["Key"] as? String else {
NSLog("Key not found")
continue
}
defaultsToRegister[key] = preference["DefaultValue"]
}
UserDefaults.standard.register(defaults: defaultsToRegister)
}
I recommend running it as early as possible. You will be sure that the defaults are there for all parts of your app.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
{
registerDefaultsFromSettingsBundle()
let one = UserDefaults.standard.bool(forKey: "switch_one")
let two = UserDefaults.standard.bool(forKey: "switch_two")
NSLog("One: \(one), Two: \(two)")
return true
}

this does not get me the default values (which are all true, but they
all return false)
Looks like you have a toggle switch which displays as ON in the setting bundle and when you read bundle you get all the false value.
If this is the case then you are missing something here.
In setting bundle(Root.plist) we have "Default Value" field which is nothing to do with the actual default value of toggle switch. This is just a visual indicator to switch.
You may have "Default value" set as "YES" in plist but when you try to read the value you will end up getting false.
Here I have set Default Value for Reminder in Root.plist as YES and for Update NO
So that when app launch it shows as above.
But when I tried to read these defaults - it gives both as false.
func getDefaults() {
let stanDefaults = UserDefaults.standard
print("Default value of Update - \(stanDefaults.bool(forKey: "update_lot_pref"))")
print("\nDefault value of Reminder - \(stanDefaults.bool(forKey: "reminder_pref"))")
}
Default value of Update - false Default value of Reminder - false
Now, if you want to sync these values - default value in Root.plist and value of default - then you have to set it programmatically.
func setApplicationDefault() {
let stanDefaults = UserDefaults.standard
let appDefaults = ["reminder_pref": true]
stanDefaults.register(defaults: appDefaults)
stanDefaults.synchronize()
}
Here in my Root.plist I have default value as YES and also when viewDidload I set this preference value as true. When I run it gives me
Default value of Reminder - true
And this is how my Root.plist looks.
Hope it helps.

Here's an answer based on #Kamil (many thanks for that) that doesn't rely on NSDictionary and uses PropertyListSerialization.
Swift 5
func registerDefaultsFromSettingsBundle() {
let settingsName = "Settings"
let settingsExtension = "bundle"
let settingsRootPlist = "Root.plist"
let settingsPreferencesItems = "PreferenceSpecifiers"
let settingsPreferenceKey = "Key"
let settingsPreferenceDefaultValue = "DefaultValue"
guard let settingsBundleURL = Bundle.main.url(forResource: settingsName, withExtension: settingsExtension),
let settingsData = try? Data(contentsOf: settingsBundleURL.appendingPathComponent(settingsRootPlist)),
let settingsPlist = try? PropertyListSerialization.propertyList(
from: settingsData,
options: [],
format: nil) as? [String: Any],
let settingsPreferences = settingsPlist[settingsPreferencesItems] as? [[String: Any]] else {
return
}
var defaultsToRegister = [String: Any]()
settingsPreferences.forEach { preference in
if let key = preference[settingsPreferenceKey] as? String {
defaultsToRegister[key] = preference[settingsPreferenceDefaultValue]
}
}
UserDefaults.standard.register(defaults: defaultsToRegister)
}
So for a Root.plist like this:
... the defaultsToRegister would be:
There's also the new compactMapValues() API in Swift 5 that may or may not be helpful here.

Add notification for UserDefaults.didChangeNotification like below:
override func viewDidLoad() {
super.viewDidLoad()
registerSettingsBundle()
NotificationCenter.default.addObserver(self, selector: #selector(updateDisplayFromDefaults), name: UserDefaults.didChangeNotification, object: nil)
updateDisplayFromDefaults()
}
func registerSettingsBundle(){
let appDefaults = [String:AnyObject]()
UserDefaults.standard.register(defaults: appDefaults)
UserDefaults.standard.synchronize()
}
func updateDisplayFromDefaults(){
let defaults = UserDefaults.standard
let update_lot = defaults.bool(forKey: "update_lot")
print(update_lot)
}

I am having a little bit of trouble following exactly what you're asking, but it sounds like you've created a settings bundle plist that specifies that some defaults are "true" if undefined (so that if a user hasn't set them they default to true). But I think when you do:
let update_lot = defaults.bool(forKey: "update_lot")
...there is no way for this code to know that "unset" should evaluate to "true" instead of "false". You could use defaults.object(forkey: "update_lot") instead and if that returns an object, get the boolean value of that object (or just call defaults.bool at that point), but if it returns nil assume that it's true.

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.

Cache server data globally and refresh views

I'm building an app that uses firebase for authentication and database functionality. Once a user signs up, a database record is stored for that user, containing some basic information like first name, last name etc.
Once a user logs in with his credentials I want to set a global variable (perhaps userDefaults?) which contains the user data for that specific user. Otherwise I have to fetch user data for every time I want to fill a label with for instance, a user's first name.
I managed to set userdefaults upon login and use this info in UIlables. But when I let users make changes to their data, of which some is important for the functioning of the app, I can update the server AND the userdefaults but the app itself doesn't update with the correct data. It keeps the old data in (for example) UIlables.
I would love to get some more insight on what the best work-flow is to manage situations like these.
When opening the app, i have a tabBarController set as rootviewcontroller. In the load of tabbarcontroller I have the following code retrieving the user data from firebase and saving it to userdefaults:
guard let uid = Auth.auth().currentUser?.uid else { return }
Database.database().reference().child("users").child(uid).observeSingleEvent(of: .value, with: { (snapshot) in
print(snapshot.value ?? "")
guard let dictionary = snapshot.value as? [String: Any] else { return }
let firstname = dictionary["First name"] as? String
let lastname = dictionary["Last name"] as? String
print("first name is: " + firstname!)
UserDefaults.standard.set(firstname, forKey: "userFirstName")
print(UserDefaults.standard.value(forKey: "userFirstName"))
self.setupViewControllers()
}
Then I continue on loading in all the viewcontrollers in the tabBarController:
self.setupViewControllers()
During that process the labels in those viewcontrollers get filled in with the userdefaults data.
This is an example of a label being filled in with userDefaults but not being updated upon changing of userdefaults:
let welcomeLabel: UILabel = {
let label = UILabel()
let attributedText = NSMutableAttributedString(string: "Welcome ")
attributedText.append(NSAttributedString(string: "\(UserDefaults.standard.string(forKey: "userFirstName")!)"))
label.attributedText = attributedText
label.font = UIFont.systemFont(ofSize: 30, weight: .bold)
return label
}()
this is a function i'm using to update the first name (via a textfield filled in by the user):
#objc func updateName() {
guard let uid = Auth.auth().currentUser?.uid else { return }
Database.database().reference().child("users").child(uid).updateChildValues(["First name" : updateNameField.text ?? ""])
UserDefaults.standard.set(updateNameField.text, forKey: "userFirstName")
print(UserDefaults.standard.value(forKey: "userFirstName"))
}
So you'll have to organize things first. In a new file define constants such as below. These constant will be accessible in global scope unless private
Constants.swift
private let storedusername = "usname"
private let storedName = "uname"
private let displaypic = "udp"
private let aboutme = "udesc"
var myusername : String {
get {
return (UserDefaults.standard.string(forKey: storedusername)!)
} set {
UserDefaults.standard.set(newValue, forKey: storedusername)
}
}
var myname : String {
get {
return (UserDefaults.standard.string(forKey: storedName)!)
} set {
UserDefaults.standard.set(newValue, forKey: storedName)
}
}
var myProfileImage : Data {
get {
return (UserDefaults.standard.data(forKey: displaypic)!)
} set {
UserDefaults.standard.set(newValue, forKey: displaypic)
}
}
var myAboutMe : String? {
get {
return (UserDefaults.standard.string(forKey: aboutme)!)
} set {
UserDefaults.standard.set(newValue, forKey: aboutme)
}
}
Now the next time you want to save anything in UserDefaults, you'll just do the following anywhere throughout your code base :
myusername = "#CVEIjk"
And to retrive it, just call it :
print(myusername)
IMPORTANT NOTE --
Always remember to initialize them. You can do this as the user signs up. As soon as they fill out their details and hit submit, just save them to these variables. That wouldn't cause unnecessary crash.
You'll have to save them at every location you perform updates regarding these nodes in the database.
Now, the refreshing views part. I am taking a scenario where your ProfileView.swift has the view and user goes to EditProfile.swift for updating the content.
You initialize all your observers the place where the update will have the immediate effect. Because the view immediately after the update matters. The rest will be called through the getter of the aboutme
ProfileView.swift
func openEditView() {
NotificationCenter.default.addObserver(self, selector: #selector(fetchUserDetails), name: Notification.Name("update"), object: nil)
//setting this right before the segue will create an observer specifically and exclusively for updates. Hence you don't have to worry about the extra observers.
perform(segue: With Identifier:)// Goes to the editProfile page
}
This function will be initially called in viewDidLoad(). At this time you need to make sure you have all the data, else it will produce no values. But if you are storing everything as the user signs up, you are safe.
#objc func fetchUserDetails() {
if uid != nil {
if myname.count > 0 { // This will check if the variable has anything in the memory or not. Dont confuse this with [Array].count
self.nameLabel = myname
}
}
}
This function also acts an ab observer method. So when the notifications are posted they can run again.
Now, EditProfile.swift
In the block where you are updating the server, save the values and then create a Notification.post and put this method right before you dismiss(toViewController:)
func updateUserCacheData(name: String, username: String, aboutme: String, ProfilePhoto: UIImage? = nil) {
DispatchQueue.global().async {
myname = name
myusername = username
myAboutMe = aboutme
if self.newImage != nil {
myProfileImage = self.newImage!.jpegData(compressionQuality: 1)!
}
DispatchQueue.main.async {
NotificationCenter.default.post(name: .refreshProfileViews, object: nil)
}
}
}
func updateToServerAndBackToProfileView() {
self.updateUserCacheData(name: iname!, username: iusername, aboutme: iaboutme!)
self.dismiss(animated: true, completion: nil)
}
}
As long as this goes back to ProfileView, your views will be instantly refreshed. You can keep an observer wherever you view will be first displayed after the dismiss. the rest will fetch updated content always. Also, don't forget to deinit your Observer in ProfileView
//This code is in ProfileView.swift
deinit {
NotificationCenter.default.removeObserver(self, name: Notification.Name("update"), object: nil)
}
Also, in cases where the content might be empty, simply initialize it with empty content. For example, if user doesn't choose to add aboutme while signing up, you can just put
`myaboutme = ""`
This will create a safe environment for you and you are well set.

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)
}
}

Values replacing each other using UserDefaults

I have an app where I time myself and see how long it takes me to complete a bunch of questions. I have the time transferred to another VC and displayed in a label. I have it being stored by pressing a button but when i have a new variable(time) it replaces it. How do i store an Array of values and that can be displayed in a label?
Button to save the value:
#IBAction func saveScore(_ sender: Any) {
scoreLabel.text = label.text
UserDefaults.standard.set(scoreLabel.text, forKey: "score")
}
The code that permanently holds the data:
override func viewDidAppear(_ animated: Bool) {
if let x = UserDefaults.standard.object(forKey: "score") as? String {
scoreLabel.text = x
}
}
My scoreLabel displays all my scores and label shows the time you just got.
Use the following extentions on UserDefaults to store an array of times:
extension UserDefaults {
var times: [String] {
get {
if let times = UserDefaults.standard.object(forKey: "times") as? [String] {
return times
} else {
return []
}
}
set {
UserDefaults.standard.set(newValue, forKey: "times")
}
}
}
While you don't need to extend UserDefaults, using extension can simplify a bit working with persisted values and it makes the code cleaner.
Then at the point where you show the data, use the following line to access the array:
let arrayOfTimes = UserDefaults.standard.times
scoreLabel.text = "\(arrayOfTimes)" // or any other formatting you'd like
And instead of setting the times to persist a new score, just add the new score to the array, e.g.:
// This will not only add the scoreLabel.text to the array, but also persists it
UserDefaults.standard.times.append(scoreLabel.text)
In Swift 4,
To save an array to User Defaults you would do:
let defaults = UserDefaults.standard
let array = [25, 50]
defaults.set(array, forKey: "Scores")
And to access the array from User Defaults:
let defaults = UserDefaults.standard
let retrievedArray = defaults.array(forKey: "Scores") as? [Int] ?? []
And if you were to display a score of your array in a label then, you would just do:
scoreLabel.text = String(describing: retrievedArray[0])
If you are using integers for your scoring system, I would suggest you
storing your scores as Int in User Defaults.
If you prefer using Strings though, please note that you can use the User Defaults' stringArray(forKey:) method directly, instead of the array(forKey:) method, and therefore, in that case, you wouldn't need to type cast your array:
let someStringArray = defaults.stringArray(forKey: "ArrayOfStrings")
Note: To answer your question, I will consider that you are using Int scores, but feel free to use whichever you prefer.
If you want to store your array to the same key in User Defaults every time you get a new score, you could do it easily like this:
let defaults = UserDefaults.standard
// Your new score:
let newScore = 75
// Get your current scores list from User Defaults:
var currentArray = defaults.array(forKey: "Scores") as? [Int] ?? []
// Append your new score to the current array:
let updatedArray = currentArray.append(newScore)
// And save your updated array to User Defaults:
defaults.set(updatedArray, forKey: "Scores")
// In this example, your User Defaults now contains the updated array [25, 50, 75]
And that's it :).
Please note that there is no need to use an extension for that..
UPDATE: Also, if you want to add something inside your viewDidAppear method, don't forget to add super.viewDidAppear(animated). The same goes for viewDidLoad, etc.
The documentation states:
You can override this method to perform additional tasks associated
with presenting the view. If you override this method, you must call
super at some point in your implementation.
So you would have:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let retrievedArray = defaults.array(forKey: "Scores") as? [Int] {
print(retrievedArray)
// You can access your scores array safely here
}
}

Change App Localization Based on User Selection

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)

Resources