While Property Lists can be ways of storing data, there is a better way of handling small amounts of data for preferences and settings that might change over time and user. While built on a property list, NSUSerDefaults
Makes for a more convenient way for such data. IN this lesson, we’ll introduce NSUserDefaults
and a few more classes which will be helpful is storing several types of data we weren’t transferring with a basic property list.
Make a New Project
We’ll write an app that changes the background color using sliders. Our app will save our settings for color, a selected segment and the date we saved the configuration. Open Xcode and make a new project named SwiftUserDefaultsDemo, using a single view template and Swift as the language. Open the storyboard and roughly set it up like this:
The background is Light Gray(#AAAAAA). For the segmented control I added two more segments, and I set the tint to Black(#000000). I added the three sliders and used a Red(#FF0000), Green(#00FF00), and Blue(#0000FF) thumb to indicate their function.
Just to keep this clean, I’ll use stack views. You can skip this if you want a messier app. If you are not familiar with the beauty that is stack views, you can check out this tutorial. Start by selecting the Dark Text Label and the switch. Click the stack view button at the bottom of the storyboard.
We will use the default values of horizontal stack view, with Fill for Alignment and Distribution. Select the switch. Go to the size inspector and change the Content Huggingfor both Horizontal and Vertical to Required(1000)
Marquee-select all the controls on the storyboard. Click the stack view button again. In the attributes inspector, set the stack view attributes to this:
Pin the stack view using the auto layout pin button 10 top, 10 left and 10 right. Select the Update Frames with Items of new constraints at the bottom of the popup then click Add 3 constraints.
Finally add a button on the bottom of the storyboard. Make the background of the button a Dark Gray(#555555) and the text White(#FFFFFF). Set the text to Save. Using auto layout, Pin the button 10 left, 10 right and 20 down. Set the Height to 60 points. Select the Update Frames with Items of new constraints at the bottom of the popup then click Add 4 Constraints. Your final layout should look like this:
Set Up Outlets and Actions
Go to the ViewController.swift file. Add the following properties, outlets and actions:
//MARK: Outlets and properties var dateUsed = NSDate() @IBOutlet weak var dateLabel: UILabel! @IBOutlet weak var segmentedControl: UISegmentedControl! @IBOutlet weak var redSlider: UISlider! @IBOutlet weak var greenSlider: UISlider! @IBOutlet weak var blueSlider: UISlider! @IBOutlet weak var textColorSwitch: UISwitch! @IBOutlet weak var rgbLabel: UILabel! @IBOutlet weak var darkTextLabel: UILabel! //MARK: - Actions @IBAction func textColorSwitch(sender: UISwitch) { } @IBAction func colorSliderChange(sender: UISlider) { } @IBAction func saveButton(sender: UIButton) { }
Go to the storyboard and open up the assistant editor. You can close the navigator and the attributes inspector to give yourself more room. From the circle next to each outlet, drag to the appropriate control on the storyboard. Do the same for the textColorSwitch
and the saveButton
actions. For the colorSliderChange
action, drag three times, once to each of the three sliders. We’ll control all three sliders with the same code.
Code the Demo App
Close the assistant editor. Open the navigator and head over to the ViewController.swift file. Code the textColorSwitch
and the colorSliderChange
like this:
@IBAction func textColorSwitch(sender: UISwitch) { updateColor(background: true) } @IBAction func colorSliderChange(sender: UISlider) { updateColor(background: true) }
Next we’ll code this new function updateColor
. Add the following under the actions:
//MARK: - Instance Methods func updateColor( background background:Bool){ //update background color if background{ view.backgroundColor = UIColor( red: CGFloat(redSlider.value), green: CGFloat(greenSlider.value), blue: CGFloat(blueSlider.value), alpha: 1.0) } //update the text color var textColor = UIColor() if textColorSwitch.on{ textColor = UIColor.blackColor() } else { textColor = UIColor.whiteColor() } dateLabel.textColor = textColor darkTextLabel.textColor = textColor rgbLabel.textColor = textColor segmentedControl.tintColor = textColor }
This is just a quick way of updating our background and text colors depending on the values for our controls. There’s a Bool
parameter background
for something we’ll test later. We now have enough code to check our switch and sliders. Build and run. You should get this:
Move the sliders around, and the background color changes.
Slide the sliders to the left to make a dark background. Change the text switch.
Our sliders and switch work. We also should update the RGB label and date label. At the bottom of the updateColor
action, add this:
//update label for RGB color rgbLabel.text = String( format: "RGB: %1.2f, %1.2f %1.2f", redSlider.value, greenSlider.value, blueSlider.value)
In viewDidLoad
, add this:
override func viewDidLoad() { super.viewDidLoad() dateLabel.text = formattedDate(dateUsed) }
This adds another function, a date formatter to our code. Add this to your code:
func formattedDate(date:NSDate) -> String{ let formatter = NSDateFormatter() formatter.timeStyle = .ShortStyle formatter.dateStyle = .MediumStyle return formatter.stringFromDate(date) }
This gives us a medium style date and a short style time with a default locale. If you are not familiar with date formatters, you might want to check out my post on them. We’re ready to build and run again. Move the sliders a bit and we get this:
Saving Preferences with NSUserDefaults
Up to this point our app starts with the gray background. We’ll use NSUserDefaults
to save our preferences for a background and other settings. NSUserDefaults
is essentially a wrapper around a property list that has easier to use methods than a straight property list. We make a property list object, then use methods to save or retrieve the type of data we have.
Add this to our properties for the ViewController
class:
let defaults = NSUserDefaults.standardUserDefaults()
This uses the class method standardUserDefaults
. This does one of two things: Creates the standard defaults file and configures it if it does not exist, or opens it if it does. For most cases you’ll use this to make your defaults.
To add a default setting to the NSUserDefaults
, we use a method for the type we are setting. In the saveButton
action, add the following:
@IBAction func saveButton(sender: UIButton) { defaults.setBool(textColorSwitch.on, forKey: "DarkText") }
This adds the textColorSwitch
‘s Bool
value on
to a key named DarkText
. Note for demonstration purposes I’m using a string literal for the key. It’s a good practice to use string constants instead to prevent several bugs due to misspellings of keys.
In earlier versions of iOS, you would then use the synchronize
method to save this. In most cases where you are coding in Swift, this is unnecessary. According to Apple, synchronize
is ultimately counter-productive, which is why we won’t use it here. synchronize
is now a scheduled event, and will happen eventually. You’ll therefore see the notable exception: If you are exiting an app and wanting to save your preferences before closing, you would need a synchronize
.
We can also add entries for Int
and Float
. Change saveButton
to this:
@IBAction func saveButton(sender: UIButton) { defaults.setBool(textColorSwitch.on, forKey: "DarkText") defaults.setInteger(segmentedControl.selectedSegmentIndex, forKey: "SegmentIndex") defaults.setFloat(redSlider.value, forKey: "Red") defaults.setFloat(greenSlider.value, forKey: "Green") defaults.setFloat(blueSlider.value, forKey: "Blue")
We’ve added the positions of the sliders and the segment index of our segmented control. Except for the name of the method, there’s little difference in their use from setBool
. This is why NSUserDefaults
is so much better for defaults than setting up your own property list. Much of the hard work is done for you. There are several methods for setting and getting data into NSUserDefaults
. Here’s a table of them:
The setObject method and NSDate
Most of the simple data types we already talked about such as Bool
and Float
are on the chart above. A few types such as String
and NSDate
don’t have their own setter, but use setObject
. These types are that same types we encountered in property lists. I’ll refer to them as property list objects. The property list objects NSData
, NSDate
, NSNumber
, String
, arrays, and dictionaries all go into user defaults using setObject
.
In our app, we will use a date example. Add another line to our setters:
defaults.setObject(NSDate(), forKey: "Date")
While the current app grabs the date and time we started the app, this will remember the last time we saved the app.
Saving Colors and Other Objects with NSData.
We’ve already stored enough information to make a color with the float values we stored. This may get cumbersome with more than one color. It makes more sense to store the color directly. Like UIColor
, we might want to store objects in our user preferences that are not property list objects. The trick here is the class NSData
. You can convert most objects to NSData
. We’ll convert the background color to NSData
and then store it in the preferences. Add this to our saveButton
action:
//Storing the color and any other non-data object let color = view.backgroundColor! let colorData = NSKeyedArchiver.archivedDataWithRootObject(color) defaults.setObject(colorData, forKey: "Color")
The first line gets our color form the view’s background. The second line uses a class method of NSKeyedArchiver to archive the object. What’s going on under the hood is beyond the scope of this tutorial, but essentially it makes an easily readable binary object. This is an NSData
object, which we can use setObject
to make it a user preference.
A word of caution about images. You can encode images as NSData
, but don’t do it. They take up a lot of space and take a long time to encode and decode. A better approach for images is to store the image with a URL and then store the image’s URL in the user preferences with the setURL:forKey:
method.
This is all we need to save our settings for this app.
Getting the User Defaults
Of course saving preferences is not enough, we have to read them back as well. Make a new function readDefaults
:
func readDefaults(){ }
Let’s set the switch for text color first. Add the following code:
textColorSwitch.on = defaults.boolForKey("DarkText")
We’re reading a property list, and the property list is set up as a dictionary. We use a key DarkText
to get a Bool
value. All these getters are immutable. It makes little sense to keep them around as variables. We assign that value directly to the switch’s on
property. We can do the same for the segmented control. Add this line:
segmentedControl.selectedSegmentIndex = defaults.integerForKey("SegmentIndex")
This does the same as the previous line using integerForKey
for the SegmentIndex
key to get our selected segment index. We can set the sliders using the floatForkey
method. Add this:
redSlider.value = defaults.floatForKey("Red") greenSlider.value = defaults.floatForKey("Green") blueSlider.value = defaults.floatForKey("Blue")
If you look at the chart above, you’ll notice a note about what happens for these methods when the key is not found. It returns a specific value of false
for the Bool
and 0 for the numbers. While in our app that is not tragic, it could be a problem in other apps. You will need some more code or some debugging to determine if it a value of 0 or false
or it is a missing key. Later on we’ll see what effect this has on our app.
That is not a problem with objects using setObject
. Its counterparts including objectForKey
return an optional value, where nil
is a not found object. Some people don’t use the simpler values mentioned about and use setObject
for numbers and Bool
values just to have the optional value. To use setObject
is slightly different to detect the nil
. We’ve got a NSDate
object in our prefrences as an example. We read it like this:
if let dateObject = defaults.objectForKey("Date") { let date = dateObject as! NSDate dateLabel.text = "Previous save: " + formattedDate(date) }
We optionally chain the defaults.objectForKey("Date")
to check if it exists. If it does, the code downcasts the returned result to a date. Once we have a date we change the date label. If not, we leave the default setting of the label we set in viewDidLoad
.
For our color object, things get a bit more complicated since there is no default like the date. We’ll need an else
to control for that situation. Add this to our code:
if let colorObject = defaults.objectForKey("Color") { let colorData = colorObject as! NSData let color = NSKeyedUnarchiver.unarchiveObjectWithData(colorData) as? UIColor view.backgroundColor = color updateColor(background: false) } else{ updateColor(background: true) }
I broke the steps out here. Like the NSDate
, we optionally chain to find the key. Once found, we downcast the value of colorObject
to NSData
. That’s the same as the date getter in structure. The next step is different though. The code converts the NSData
object back to a color. With that color, we update the background color. Since we don’t need the slider values to update the color for us, we call updateColor
to not update the background with a false
parameter.
Our finished function looks like this
func readDefaults(){ textColorSwitch.on = defaults.boolForKey("DarkText") segmentedControl.selectedSegmentIndex = defaults.integerForKey("SegmentIndex") redSlider.value = defaults.floatForKey("Red") greenSlider.value = defaults.floatForKey("Green") blueSlider.value = defaults.floatForKey("Blue") if let dateObject = defaults.objectForKey("Date") { let date = dateObject as! NSDate dateLabel.text = "Previous save: " + formattedDate(date) } if let colorObject = defaults.objectForKey("Color") { let colorData = colorObject as! NSData let color = NSKeyedUnarchiver.unarchiveObjectWithData(colorData) as? UIColor view.backgroundColor = color updateColor(background: false) } else{ updateColor(background: true) } }
Add the function to viewDidload
like this
override func viewDidLoad() { super.viewDidLoad() dateLabel.text = formattedDate(dateUsed) readDefaults() }
Build and run. We don’t get the gray background with black lettering:
Since the property list for NSUserDefaults
hasn’t been created yet, the values for the keys were not found. Our UIColor
wasn’t there and attempted to set the color from the three float values of the sliders. They weren’t there, and thus were 0.0,0.0,0.0, which is black. The dark text switch was false, and the index of our segments was 0, again because values were not found.
Change the color to orange by sliding the red all the way to the left and the green to 3/4 left. Click the Fourth segment in the segmented control and change the text to dark text.
Click the Save Button. Double-Click the home button. If you are using the simulator that is Command-Shift-HH.
Kill the app by dragging the app up. Then click the icon to start the app again. We get an orange screen and all of our other settings change the app, including the previous save date at the top
Dealing With the Missing Keys
That first use of the app will run into some problems due to no values for the preferences. You might want to detect that case and set some of the preferences yourself. For example, we can change this in readDefaults
:
} else{ updateColor(background: true) }
to this:
} else{ if blueSlider.value == 0 && redSlider.value == 0 && greenSlider.value == 0 { // very probable no data in defaults blueSlider.value = 0.8 greenSlider.value = 0.8 redSlider.value = 0.8 textColorSwitch.on = true } updateColor(background: true) }
We make an assumption if we don’t find the UIColor
and the sliders are all zero, there is no preferences set, and so we set them programmatically to light gray with dark text. In the simulator click on the icon for the app, and delete it. Deleting the app deletes our preferences, as the alert tells you. Build and run the app again. This time we get a gray background.
In most apps, It’s a good idea to have handlers available for missing data. There are three possibilities: there is missing data, the key is wrong, or the data type is wrong. For all three, you need to take some action.
Beyond the Settings View
Our app works for a single page. It’s more likely those defaults are for several pages. Close the app and in Xcode go to the storyboard. Click on the view controller and in the menu, select Editor>embed in>Navigation Controller. We get an navigation controller and a navigation bar in the view controller obscuring our stack view and a warning error. Click the resolver button At the bottom of the storyboard and select Update All frames. The stack view will move to the correct place.
Add a bar button item to the right side of the navigation bar. Title the button Next. Drag a view controller to the story board. Connect the Next bar button to the new view controller by control dragging from the button to the view controller. Select a Show segue from the menu. You’ll end up with a storyboard that looks like this:
Press Command-N to bring up the new file dialog box. Make a new Cocoa Touch Class named TwoViewController subclassing UIViewController with Swift as the language. In the code that appears for TwoViewController
, Chge viewDidLoad
to this:
override func viewDidLoad() { let defaults = NSUserDefaults.standardUserDefaults() super.viewDidLoad() if let colorObject = defaults.objectForKey("Color") { let colorData = colorObject as! NSData let color = NSKeyedUnarchiver.unarchiveObjectWithData(colorData) as? UIColor view.backgroundColor = color } else{ view.backgroundColor = UIColor.lightGrayColor() } }
We’ll set the background color from the defaults. If we don’t have a color, then we’ll set the background color to light gray.
Connect up this code by going back to the storyboard. Click our new view controller on the storyboard. In the identity inspector, set the view controller class to TwoViewController
. Build and run. If you havent hit Save
on the app since the last run you should have the light gray settings screen. Click Next
and you get another light gray settings screen, since we dont have a key yet.
Click back and change the color of the background with the sliders.
Tap the Save
button, then tap the next button.
Now we have our background read from the defaults.
A Few Last Thoughts on NSUserDefaults.
There’s a lot more one could cover on NSUserDefaults
. I need to add a few more suggestions about the topic. The first and most important is that NSUserDefaults and any property list is not secure. Don’t store sensitive information like passwords, financial data or health data here. It’s in XML. With not much work and access to a phone, it’s easily read.
Secondly, with NSData
you might want to run more checks on the data than I did in our example. I didn’t check here to make sure colorData
is a UIColor
, which is something you might want to do.
With objects like UIColor
, you may be adding a lot of code to a lot of view controllers to archive and unarchive them. It’s a good idea to extend NSUserDeafaults
instead and make a function call to the extention in your view controllers.
Finally, I used a save method and required the user to press Save to save the defaults. There are times you may not want that, there are times you do. You could automatically save by placing the saving method call in viewWillDisappear or even in the actions for the controls themselves (thought that might be overkill). In an automatic save situation, the save will reflect on all other view controllers as they read the new defaults. Be careful. There are many cases both with automatic saves and manual saves the view controllers besides of settings won’t know they need to make changes. In our app you can change the color and hit Next and it will show the last color saved, not the color selected. A tab bar controller wont get the message on all its view controllers if the setting controller changes something. This is where notifications comes in. In our next persistence lesson, we’ll look at notifications while exploring a settings page outside your app in Apple’s Settings app.
The Whole Code
ViewController.swift
// // ViewController.swift // SwiftUserDefaultsDemo // // Created by Steven Lipton on 2/24/16. // Copyright © 2016 MakeAppPie.Com. All rights reserved. // import UIKit class ViewController: UIViewController { //MARK: Outlets and properties var dateUsed = NSDate() let defaults = NSUserDefaults.standardUserDefaults() @IBOutlet weak var dateLabel: UILabel! @IBOutlet weak var segmentedControl: UISegmentedControl! @IBOutlet weak var redSlider: UISlider! @IBOutlet weak var greenSlider: UISlider! @IBOutlet weak var blueSlider: UISlider! @IBOutlet weak var textColorSwitch: UISwitch! @IBOutlet weak var rgbLabel: UILabel! @IBOutlet weak var darkTextLabel: UILabel! //MARK: - Actions @IBAction func textColorSwitch(sender: UISwitch) { updateColor(background: true) } @IBAction func colorSliderChange(sender: UISlider) { updateColor(background: true) } @IBAction func saveButton(sender: UIButton) { //Storing simple types defaults.setBool(textColorSwitch.on, forKey: "DarkText") defaults.setInteger(segmentedControl.selectedSegmentIndex, forKey: "SegmentIndex") defaults.setFloat(redSlider.value, forKey: "Red") defaults.setFloat(greenSlider.value, forKey: "Green") defaults.setFloat(blueSlider.value, forKey: "Blue") //Storing a data object -- NSDate, Array, Dictionary, NSData, NSString, NSNumber defaults.setObject(NSDate(), forKey: "Date") //Storing the color and any other non-data object let color = view.backgroundColor! let colorData = NSKeyedArchiver.archivedDataWithRootObject(color) defaults.setObject(colorData, forKey: "Color") } //MARK: - Instance Methods func updateColor( background background:Bool){ if background{ view.backgroundColor = UIColor( red: CGFloat(redSlider.value), green: CGFloat(greenSlider.value), blue: CGFloat(blueSlider.value), alpha: 1.0) } var textColor = UIColor() if textColorSwitch.on{ textColor = UIColor.blackColor() }else{ textColor = UIColor.whiteColor() } dateLabel.textColor = textColor darkTextLabel.textColor = textColor rgbLabel.textColor = textColor segmentedControl.tintColor = textColor //update label for RGB color rgbLabel.text = String( format: "RGB: %1.2f, %1.2f %1.2f", redSlider.value, greenSlider.value, blueSlider.value) } func formattedDate(date:NSDate) -> String{ let formatter = NSDateFormatter() formatter.timeStyle = .ShortStyle formatter.dateStyle = .MediumStyle return formatter.stringFromDate(date) } func readDefaults(){ textColorSwitch.on = defaults.boolForKey("DarkText") segmentedControl.selectedSegmentIndex = defaults.integerForKey("SegmentIndex") redSlider.value = defaults.floatForKey("Red") greenSlider.value = defaults.floatForKey("Green") blueSlider.value = defaults.floatForKey("Blue") if let dateObject = defaults.objectForKey("Date") { let date = dateObject as! NSDate dateLabel.text = "Previous save: " + formattedDate(date) } if let colorObject = defaults.objectForKey("Color") { let colorData = colorObject as! NSData let color = NSKeyedUnarchiver.unarchiveObjectWithData(colorData) as? UIColor view.backgroundColor = color updateColor(background: false) } else{ if blueSlider.value == 0 && redSlider.value == 0 && greenSlider.value == 0 { // very probable no data in prefrences blueSlider.value = 0.8 greenSlider.value = 0.8 redSlider.value = 0.8 textColorSwitch.on = true } updateColor(background: true) } } override func viewDidLoad() { super.viewDidLoad() dateLabel.text = formattedDate(dateUsed) readDefaults() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } }
TwoViewController.swift
// // TwoViewController.swift // SwiftUserDefaultsDemo // // Created by Steven Lipton on 2/26/16. // Copyright © 2016 MakeAppPie.Com. All rights reserved. // import UIKit class TwoViewController: UIViewController { override func viewDidLoad() { let defaults = NSUserDefaults.standardUserDefaults() super.viewDidLoad() if let colorObject = defaults.objectForKey("Color") { let colorData = colorObject as! NSData let color = NSKeyedUnarchiver.unarchiveObjectWithData(colorData) as? UIColor view.backgroundColor = color } else{ view.backgroundColor = UIColor.lightGrayColor() } } }
Leave a Reply