[Download Xcode 8.2.1 project with full Swift 3 source from GitHub.]
Today, we’ll cover the topic of storing a user’s “default settings” inside your app — not forcing the user to go to a Settings bundle. The key to doing this is to save the user’s preferences to a persistent store whenever the user changes those preferences (or at least when the app goes into the background, but definitely before the app is terminated). But saving/writing those preferences is not enough. You must also read those preferences from the persistent store every time the app opens so that the user will always see their default settings. User settings/preferences must be synchronized with the persistent store.
I’m talking about saving simple pieces of data, like a user’s preferences for the background color of views, preferred language (i.e., English, Hindi, Portuguese, Spanish), preferred measuring units (metric or English), etc. Apple’s Human Interface Guidelines don’t explicitly forbid in-app settings. In fact, they explicitly permit them:
Note: Apps are not required to use a Settings bundle to manage all preferences. For preferences that the user is likely to change frequently, the app can display its own custom interface for managing those preferences.
While Apple does generally encourage the use of a Settings bundle, there is a very cogent “argument for placing settings in inside your app.” It’s just very convenient to set an app’s preferences without leaving the app. My latest iOS app, accepted into the App Store by the Apple review team just a couple weeks ago, uses in-app settings. In fact, the app reviewers didn’t take issue with any features in my app.
Today, we’ll create the simple app shown in the video immediately below (Figure 1). Please watch:
Figure 1. We use two UISwitch components to change property values in realtime. We also store the isOn property, or save the result of setting the isOn property, of each UISwitch in NSUserDefaults.
Before discussing the app’s sample code, let me summarize iOS data storage options just so you know about all the possibilities. There are several tools of varying levels of functionality and complexity offered by iOS for storing information across app sessions, i.e., between multiple app startups and termination. We can roughly classify iOS state management technologies into the following categories:
- In-app user preferences
- Settings bundle for user preferences
- Preserving and restoring app user interface state
- Saving app/user data (Core Data, locally)
- Saving app/user data (remotely, in iCloud)
So let’s talk about in-app user preferences using the Apple Foundation Framework’s API class UserDefaults:
The NSUserDefaults class provides a programmatic interface for interacting with the defaults system. The defaults system allows an application to customize its behavior to match a user’s preferences. For example, you can allow users to determine what units of measurement your application displays or how often documents are automatically saved. Applications record such preferences by assigning values to a set of parameters in a user’s defaults database. The parameters are referred to as defaults since they’re commonly used to determine an application’s default state at startup or the way it acts by default. …
At runtime, you use an NSUserDefaults object to read the defaults that your application uses from a user’s defaults database. NSUserDefaults caches the information to avoid having to open the user’s defaults database each time you need a default value. The synchronize() method, which is automatically invoked at periodic intervals, keeps the in-memory cache in sync with a user’s defaults database.
When you run your app for the first time, nothing is stored in the “user’s defaults database.” I don’t have to worry about accessing an NSUserDefaults object the first time the app is run. Apple’s documentation states “If the shared defaults object does not exist yet, it is created.”
NSUserDefaults is a list of key-value pairs. All keys are of String type. All values are “types such as floats, doubles, integers, Booleans, and URLs.” You can even store more complex types like NSArray or NSDictionary, but we won’t get too fancy today. So NSUserDefaults is really easy to use. Here are some examples from Apple’s documentation:
… Setting Default Values
func set(Any?, forKey: String)
Sets the value of the specified default key in the standard application domain.
…
func set(Bool, forKey: String)Sets the value of the specified default key to the specified Boolean value.
…
Getting Default Valuesfunc object(forKey: String)
Returns the object associated with the first occurrence of the specified default.
…
func bool(forKey: String)Returns the Boolean value associated with the specified key. …
Here’s a couple of simple examples in Swift 3. We store some key/value pairs to NSUserDefaults. Then we retrieve the values using the keys and print the values to console. Read my inline commentary:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
... // this is an array of colors let arrayColorList = ["red", "green", "blue"] ... // get a copy of the shared NSUserDefaults object so we can // use short-hand and save some typing let userDefaults = UserDefaults.standard // // notice with Swift method overloading, I can pass // arguments of different types // // userDefaults.set(value: Any?, forKey: String) userDefaults.set("red", forKey: "color") // userDefaults.set(value: Any?, forKey: String) userDefaults.set(arrayColorList, forKey: "colors") ... // // note I can use methods "value" or "string" // // if let color = userDefaults.value(forKey: "color") if let color = userDefaults.string(forKey: "color") { print("\(color)\n") } if let colorList = userDefaults.array(forKey: "colors") { for color in colorList { print(color) } } |
1 2 3 4 5 |
red red green blue |
When I started developing the app shown in the video above, I implemented two UISwitchs. You know how a UISwitch works. It’s an on/off toggle. When you slide one to the right and its background turns green, it is “on” and its isOn property is set to true. If you slide a UISwitch switch to the left, it is “off” and its isOn property is set to false. You’ve probably noticed that most Settings bundles have a lot of UISwitchs. There’s a good reason for that. The UISwitch is a great way to specify whether an app property (preference) is on or off.
See Figure 2 below to review the storyboard for the app I wrote for this tutorial:
I started the project without state preservation. (You always want to encode your core functionality first.) I could change the background color of the UIView behind the UIStackView containing the “Measure: feet/meters” text using the top UISwitch. I could toggle the text between “Measure: feet” and “Measure: meters” using the bottom UISwitch. But the app couldn’t remember these changes across app termination and restart. All I could do was change the UIView’s background color and UIStackView’s text while the app was running, but that’s it. Here’s the initial Swift 3 code — without persistence:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
class ViewController: UIViewController { @IBOutlet weak var backgroundView: UIView! @IBOutlet weak var distanceMeasureLabel: UILabel! ... @IBAction func backgroundColorSwitchChanged(_ sender: Any) { // "Any can represent an instance of any type at all." // Since we created this IBAction by dragging from // a UISwitch, we can be certain that the "sender" // is a UISwitch, so we force downcast and unwrap. let swtich:UISwitch = sender as! UISwitch if swtich.isOn { backgroundView.backgroundColor = UIColor.lightGray } else { backgroundView.backgroundColor = UIColor.clear } } // end backgroundColorSwitchChanged ... @IBAction func useMetricSwitchChanged(_ sender: Any) { // "Any can represent an instance of any type at all." // Since we created this IBAction by dragging from // a UISwitch, we can be certain that the "sender" // is a UISwitch, so we force downcast and unwrap. let swtich:UISwitch = sender as! UISwitch if swtich.isOn { distanceMeasureLabel.text = "meters" } else { distanceMeasureLabel.text = "feet" } } // end useMetricSwitchChanged ... } // end ViewController |
Now I’m going to add saving of user preferences — the values of the two UISwitch components — to my Swift 3 code. Note that I haven’t said anything about reading those preferences on app startup. We’ll do that later:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
... @IBAction func backgroundColorSwitchChanged(_ sender: Any) { let userDefaults = UserDefaults.standard // "Any can represent an instance of any type at all." // Since we created this IBAction by dragging from // a UISwitch, we can be certain that the "sender" // is a UISwitch, so we force downcast and unwrap. let swtich:UISwitch = sender as! UISwitch if swtich.isOn { // since switch is on, set background color backgroundView.backgroundColor = UIColor.lightGray // save the switch's "on" value to NSUserDefaults userDefaults.set(true, forKey: "backgroundViewColorOn") // force save of NSUserDefaults to disk... // "Because this method is automatically invoked // at periodic intervals, use this method only if you // cannot wait for the automatic synchronization // (for example, if your application is about to exit) // or if you want to update the user defaults to what is on // disk even though you have not made any changes." userDefaults.synchronize() } else { // since switch is off, clear background color backgroundView.backgroundColor = UIColor.clear // save the switch's "off" value to NSUserDefaults userDefaults.set(false, forKey: "backgroundViewColorOn") // force save of NSUserDefaults to disk... // "Because this method is automatically invoked // at periodic intervals, use this method only if you // cannot wait for the automatic synchronization // (for example, if your application is about to exit) // or if you want to update the user defaults to what is on // disk even though you have not made any changes." userDefaults.synchronize() } } //end backgroundColorSwitchChanged @IBAction func useMetricSwitchChanged(_ sender: Any) { let userDefaults = UserDefaults.standard // "Any can represent an instance of any type at all." // Since we created this IBAction by dragging from // a UISwitch, we can be certain that the "sender" // is a UISwitch, so we force downcast and unwrap. let swtich:UISwitch = sender as! UISwitch if swtich.isOn { distanceMeasureLabel.text = "meters" userDefaults.set("meters", forKey: "measurementType") userDefaults.synchronize() } else { distanceMeasureLabel.text = "feet" userDefaults.set("feet", forKey: "measurementType") userDefaults.synchronize() } } // end useMetricSwitchChanged ... |
We’re saving user preferences to disk, but they won’t do us any good if we don’t read those preferences every time the app starts up. In a simple example like this, we’ll read preferences every time our one and only UIViewController is initialized (in viewDidLoad). Read my inline comments in the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. // get a copy of the shared NSUserDefaults object so we can // use short-hand and save some typing let userDefaults = UserDefaults.standard /* This is an alternative methodology for reading a default value, It returns a value if it finds one and returns false if no default is found, which to me, leaves too much open to question. You decide. let isViewBackgroundColorOn:Bool = userDefaults.bool(forKey: "backgroundViewColorOn") if isViewBackgroundColorOn { */ // if we find a preference for "backgroundViewColorOn" then read its value if let isViewBackgroundColorOn:Bool = userDefaults.value(forKey: "backgroundViewColorOn") as! Bool? { if isViewBackgroundColorOn { backgroundView.backgroundColor = UIColor.lightGray backgroundViewColorSwitch.isOn = true } else { backgroundView.backgroundColor = UIColor.clear backgroundViewColorSwitch.isOn = false } } else // no preference was found so we set a default value { backgroundView.backgroundColor = UIColor.clear backgroundViewColorSwitch.isOn = false userDefaults.set(false, forKey: "backgroundViewColorOn") userDefaults.synchronize() } // if we find a preference for "measurementType" then read its value if let isMeasureType:String = userDefaults.value(forKey: "measurementType") as! String? { if isMeasureType == "meters" { distanceMeasureLabel.text = "meters" useMetricSwitch.isOn = true } else { distanceMeasureLabel.text = "feet" useMetricSwitch.isOn = false } } else // no preference was found so we set a default value { distanceMeasureLabel.text = "feet" useMetricSwitch.isOn = false userDefaults.set("feet", forKey: "measurementType") userDefaults.synchronize() } } // end viewDidLoad() |
There you have it: Saving user preferences inside an app. The user can change their preferences at any time. And the preferences are preserved even if the app has been closed and restarted multiple times.
[Download Xcode 8.2.1 project with full Swift 3 source from GitHub.]
Hey Andrew,
Thanks for the great article! I have a question, how can I achieve the same thing using Core Data? I know that I should use UserDefaults for such a simple task, but I really need to use Core Data.
I need to persist the state of a single UISwitch. I would very much appreciate if you could point me in the right direction!
Here’s my code
I have a DataModel created that hold 1 entity: SwitchState and 1 attribute: isOn – of type Bool
My ViewController,
1. The variable that will store the fetched request.
var switchArray = [SwitchState]()
2. Store the viewContext
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
3. IBAction when the switch is turned on/off
@IBAction func changeTemp(_ sender: UISwitch) {
let switchState = SwitchState(context: context)
switchState.isOn = true
if sender.isOn {
// Idealy I’d have to use something like: switchArray.something to get to .isOn atribute = true
switchState.isOn = true
saveItems()
updateUIWithWeatherData(temperatureIn: “\(weatherDataModel.temperature * Int(1.8) + 32)°F”)
} else {
// Idealy I’d have to use something like: switchArray.something to get to .isOn atribute = false
switchState.isOn = false
saveItems()
updateUIWithWeatherData(temperatureIn: “\(weatherDataModel.temperature)°C”)
}
}
4. The save method
func saveItems() {
do {
try context.save()
} catch {
print(“Error saving context: \(error)”)
}
}
So far it is saving the data in the sqlite properly. 0 for off state and 1 for on state.
5. Here comes the trouble 🙂
func loadItems() {
let request: NSFetchRequest = SwitchState.fetchRequest()
do {
switchArray = try context.fetch(request)
} catch {
print(“Error fetching data from context: \(error)”)
}
}
How can I access the isOn attribute in SwitchState if I save the fetched request into an array?(because context.fetch(request) always returns an array)
Ion:
Thanks for visiting my blog. When starting with a technology like Core Data, you need to go through a good tutorial. Here are some quality tutorials with sample code included: