So, let’s say we have an app that needs to remember a few simple things that the user puts in when they first load the app. It needs to remember the user’s name and birthday, to show on some view controller, or maybe even for a countdown on their Apple Watch.
There are plenty of ways to save data for your app. Some are easy to use, but rather limited, while others are much harder to use, but give you a lot more capabilities. Today, we are going to cover something on the easy, but limited end of the spectrum. For the app mentioned above, the information we’re storing will be used to set this app up with default values, for this user anyway.
That is why this method is called NSUserDefaults. It has its limitations, but it is very easy to use, and is ideal for simple storage of things like Strings and numbers.
Storable Types in NSUserDefaults
The NSUserDefaults class acts very much like something called a Property List (aka plist). It may be just a fancy interface for a plist, or it may be more, I’m not entirely sure. Nonetheless, plists are limited in what kind of objects they can store. The six types plists can store are:
- NSData
- NSString
- NSNumber
- NSDate
- NSArray
- NSDictionary
Just to make it clear, since they only differ by one letter, the first one listed as NSData, while the fourth one is an NSDate. A property list, or NSUserDefaults can store any type of object that can be converted to an NSData object. It would require any custom class to implement that capability, but if it does, that can be stored as an NSData. These are the only types that can be stored directly. Thankfully, Swift Strings, Arrays, and Dictionaries are automatically converted to their NS counterparts, so they can be stored in here as well.
An NSNumber is an NSObject that can contain the original C style numeric types, which even include the C/Objective-C style BOOL type (that stores YES or NO). Thankfully for us, these are also bridged to Swift, so for a Swift program, an NSNumber can automatically accept the following Swift types:
- UInt
- Int
- Float
- Double
- Bool
That final one is that Swift style Bool that stores the value of “true” or “false”. Nonetheless, if we want to expand this out slightly to show The Swift versions instead of their Objective-C forms, where applicable, you can store the types:
- Data
- String
- NSNumber
- UInt
- Int
- Float
- Double
- Bool
- Date
- Array
- Dictionary
Writing to NSUserDefaults
First, to read from or write to NSUserDefaults, you have to get a reference to it. In the simplest use of NSUserDefaults, this is done via a method that returns a reference to an object capable of interacting with NSUserDefaults’s data store. This method returns something very much like a singleton. A singleton is an object that only has one instance of it generated per program. When you call the class method the first time, the object is instantiated and a reference is passed back. When you call that method again, it does not instantiate a new one, and passes the aforementioned reference again, making both point to the same instance. Singletons are often viewed as an anti-pattern, particularly since they are difficult to test with. They are often used to give global state to apps, which hide dependencies within the single reference to a large object, instead of having them explicitly injected through a type’s public interface or protocol.
Nonetheless, we are not covering singletons in general today, but just be aware that while this is effectively a singleton, you should be careful about using the design pattern yourself. When I use NSUserDefaults, I have had all of its accesses tucked away in a single class, that if I want to test, I can just replace that single class with another one that adopts the same protocol that has the values I want to test with (and thus side-step using NSUserDefaults altogether in the test, I’m not supposed to be testing NSUserDefaults itself).
Anyway, all that to say that we basically need to get a reference to NSUserDefaults, and then ask it to save something. Below is sample code of how to do that:
let defaults = UserDefaults.standard defaults.set("Coding Explorer", forKey: "userNameKey")
So, the first line is getting a reference to something that can access NSUserDefaults with the standard class property. After that, we tell it to set an object with a specific key. In this case, we are storing the user name, which I put in as “Coding Explorer”, and then give that a key to use to recall it later, similar to using a Dictionary.
There are several convenience methods for storing data in NSUserDefaults they include:
- func set(_ value: Bool, forKey defaultName: String)
- func set(_ value: Int, forKey defaultName: String)
- func set(_ value: Float, forKey defaultName: String)
- func set(_ value: Double, forKey defaultName: String)
- func set(_ value: Any?, forKey defaultName: String)
- func set(_ url: URL?, forKey defaultName: String)
Reading from NSUserDefaults
Reading is done in a very similar fashion. You need to get a reference to the NSUserDefaults object, and then ask it for the value you want. In the case of reading out the String we wrote in and printing it to the console, we would use the code:
let defaults = UserDefaults.standard if let name = defaults.string(forKey: "userNameKey") { print(name) }
The methods to read something out, similar to a dictionary, return an Optional of whatever you are looking for, if it can. That way, if something does NOT exist for the key you used, it would return nil. So, in this example, we optionally bind the output of stringForKey(“userNameKey”) to the constant “name”. If it exists at that key, then we store it in the name constant, and go into the if statement and proceed with the code in there (to print the “name” String). Otherwise, if a value is not found for that key, that if statement results in a false and thus we don’t try to access the value since it is actually nil.
Much like writing to NSUserDefaults, there are several convenience methods for reading data back out, that are a bit more full featured than writing them:
- func boolForKey(defaultName: String) -> Bool
- func integerForKey(defaultName: String) -> Int
- func floatForKey(defaultName: String) -> Float
- func doubleForKey(defaultName: String) -> Double
- func objectForKey(defaultName: String) -> AnyObject?
- func URLForKey(defaultName: String) -> NSURL?
- func dataForKey(defaultName: String) -> NSData?
- func stringForKey(defaultName: String) -> String?
- func stringArrayForKey(defaultName: String) -> [String]?
- func arrayForKey(defaultName: String) -> [AnyObject]?
- func dictionaryForKey(defaultName: String) -> [String : AnyObject]?
Since this is an Objective-C class there are some caveats to how this works, that makes it a bit odd for Swift. First, you will notice that the ones that read Bool, Int, Float, and Double do not return Optionals. In Objective-C, this returns the C primitive types for those variables. As such, they could not be set to nil, since they weren’t Objective-C objects. In those cases, they return sentinel values appropriate to their types which are false (or NO), 0, 0.0, and 0.0 respectively. Maybe someday this class will be modified to have them return Optionals in Swift eventually, but for now, this is how they work.
Next, in Objective-C, NSArrays and NSDictionaries could store any type of object in them. You could have an NSArray holding types like NSInteger, NSViewController, UIImage, or UIActivityViewController all in the same NSArray. As such, the types coming out of NSArrays or NSDictionaries were of the Objective-C type “id”, which translates to “ANYObject” in Swift. That’s why stringArrayForKey, arrayForKey, and dictionaryForKey return with AnyObject in their return types. In the case of objectForKey, that is exactly what you’re asking for, so that one makes sense.
In the case of storing an NSDate, you will have to store it as an NSObject, and type-cast it back to an NSDate when you read it back out. For any other type of value not included in the major plist storage types, you will have to encode it into an NSData, write that with setObject:ForKey:, and read it back with dataForKey:. I have not learned much about NSCoding yet, but that is what you would use for archiving custom classes to NSData.
Use Constants for your Keys
Now, for simplicity, I wrote the Swift String literal each time I wrote the key. I didn’t want to set up what I’m about to say beforehand just to make the code to access a value from NSUserDefaults as easy to understand as possible (to know for sure there is a String there).
Doing it THAT way in production code is a very bad idea.
Instead, you should create a constant for that value, and assign the Swift String Literal to it. This helps for a few reasons:
- If you need to change the literal for some reason, you only have to change it in one place.
- Xcode can now help autocomplete the constant’s name in your method call.
- If you make a typo with the constant name, you get a compile-time error (while a typo in a String literal is a run-time error).
So, here is some example code that has 2 buttons running the previous code, but with a constant defined:
let userNameKeyConstant = "userNameKey" @IBAction func writeButtonWasTapped(_ sender: UIButton) { let defaults = UserDefaults.standard defaults.set("Coding Explorer", forKey: "userNameKey") } @IBAction func readButtonWasTapped(_ sender: UIButton) { let defaults = UserDefaults.standard if let name = defaults.string(forKey: userNameKeyConstant) { print(name) } }
I actually tend to have the value stored in the key constant be the same as the variable name, but I wanted to show here clearly that you don’t have to (the constant is named “userNameKeyConstant” while the actual String stored within is “userNameKey”). As you can see, the code is pretty much the same, just with that new constant instead of the String literal. I defined the constant at the class scope (which means within the class’s curly braces, the class declaration was not shown in this example), and used it in the individual methods.
Conclusion
NSUserDefaults is ideal for storing small bits of information between app launches. Its API is very simple, and does its job well. It does show its Objective-C roots in Swift moreso than some other classes, but it still gets the job done.
It is best when used for little information like user preferences, but not for larger things like UIImages. If want to store more complex things, it is better to use some of the more full-featured forms of data persistence. You could work directly with a plist yourself, write the file to disk, or even use Core Data. We can cover these later, but I figured starting with the simplest form would be best.
NSUserDefaults is in general read from, and written to, atomically. This is good for making sure the data is read and written correctly, but as such it is ALL written when something changes. So if you have something in there, and you change something small like a bool, the whole thing needs to be written again, so even though only a small part has changed, you’re still writing back out that large part. Same thing for reading. If you only needed to read that boolean, when NSUserDefaults first loads, it will still read everything, and then just give you the bool you requested. It is cached after the initial load, so it shouldn’t be too slow afterwards, but the initial read could be quite slow with larger amounts of data.
There is another reason for looking into NSUserDefaults. Using NSUserDefaults in combination with App Groups is the simplest way to share data between your app and any extensions it has. We have not covered that in this post, it is already over 2,000 words long at this point, so we’ll cover that in a more specialized post. Extensions should load quickly, so you still don’t want to put much in there, but preferences like Strings are easily shared between apps and their extensions using this method.
I hope you found this article helpful. If you did, please don’t hesitate to share this post on Twitter or your social media of choice, every share helps. Of course, if you have any questions, don’t hesitate to contact me on the Contact Page, or on Twitter @CodingExplorer, and I’ll see what I can do. Thanks!