Hello everyone! It's time to get our hands dirty with swift.
Today we’re going to be looking at how we can save data into a .plist file as well as get it back from that file.
But first things first, what is actually “Local Data Persistency” and why is it important for app developers? Feel free to skip that part if you are already familiar with that term.
The Nature of Local Data Persistency
Let’s imagine we want to create a counting app. That’s what it could look like:
As you can see the UI is extremely simple. We’ve got an add button on the lower half of the screen, and at the top, there is a text label that shows how many times the button has been pressed.
As long as, you don’t force quit the app by double-clicking on the home button (swiping up for the buttonless generation) and removing it by swiping up, or stopping the program in Xcode, the number will keep its value. So if you hit the button 5 times then 5 will be displayed.
But what if you do quit the app?
Then the app gets reset if the data is not saved beforehand. So the next time you restart the app, the text label will show you 0 again.
So how can we fix this?
One way how we can save data persistently on our device or the simulator (therefore local) and reload it when the app gets restarted is through User Defaults.
User Defaults
User defaults are used to quickly persist small bits of data like e.g. scores, player nickname, music on/off, etc. These pieces of information will be stored inside a .plist file.
The datatypes that User Defaults supports are Bools, Dictionaries, Strings, Ints, Floats, Doubles, Data, and Arrays — so, all standard data types.
Saving an Int
UserDefaults.standard.set(18, forKey: "age")
Retrieving an Int
var age = UserDefaults.standard.integer(forKey: "age")
The parameter forKey: “age” is the key used to save and retrieve data. It is similar to the key and value principle in dictionaries. “age” is the key, and 18 the value.
Here’s an example of how User Defaults is used in the counting app:
If you want to download the project you can do that here.
Another way how you can save data is through Codable.
Codable
Why Codable? Because it enables you to flash freeze custom objects. So, you can create your own data models with structures, and save it inside a .plist file.
By the way, Codable is actually a type alias (like a generic term) of the Decodable and the Encodable protocol. To apply both protocols to a struct or a class using the type alias Codable. It’s shorter and saves you a bit of time ;)
How does codable work?
The principle can be compared to a disc containing e.g. hard rock music and the music player. When a songwriter finishes his song, he’s going to record it in the next step. The recorder converts (encodes) the music into a format that only computers understand.
After finishing the recording the musician wants to listen to it, so he clicks play on his computer. The zeros and ones get converted back (decoded) into music notes, and the musician can enjoy his or her masterpiece.
That’s it! With the “codable method”, we are exactly doing the same. And here’s how it’s done in code:
Codable in Action
import Foundation
//Array where all the food Items are listed
var foodArray: [Item] = []
//adds the Item.plist file to the documentDirectory
let dataFilePath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("Item.plist")
print("Data File Path: \(dataFilePath!)")
//DataModel that defines what an food Item has as properties
struct Item: Codable {
var title: String = ""
var amount: Int = 0
}
//Coder: Encodes and Decodes data
class Coder {
func retrieveData() {
let decoder = PropertyListDecoder()
do {
let data = try Data(contentsOf: dataFilePath!)
foodArray = try decoder.decode([Item].self, from: data)
} catch {
print("Error with getting data, \(error)")
}
print("Food Items: \(foodArray)")
}
func saveData() {
let encoder = PropertyListEncoder()
do {
let data = try encoder.encode(foodArray)
try data.write(to: dataFilePath!)
} catch {
print("Error with saving data, \(error)")
}
}
}
//initialize coder
var coder = Coder()
//Items
var newItem = Item()
newItem.title = "Vegetables"
newItem.amount = 10
foodArray.append(newItem)
var newItem2 = Item()
newItem2.title = "fruits"
newItem2.amount = 20
foodArray.append(newItem2)
var newItem3 = Item()
newItem3.title = "Chocolate Cakes"
newItem3.amount = 11
foodArray.append(newItem3)
//encoding data
coder.saveData()
//decoding data
coder.retrieveData() //Output should be the values of newItem, newItem2, newItem3
//adding an other food item
var newItem4 = Item()
newItem4.title = "milk"
newItem4.amount = 5
foodArray.append(newItem4)
//Encoding Data
coder.saveData()
//Decoding data
coder.retrieveData() //Now the Item.plist file should contain 4 items where the last is "
Let’s see what we’ve got here.
The first thing we should look at is this code snippet here:
let dataFilePath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("Item.plist")
Through this, we can create a file called Item.plist that is locally stored on the device. This Item.plist is the place where we are going to save the data.
Create a Data Model
Next, we have our structure called Item, which represents the data model in this example. It defines all properties that a food item has. In this case, we’ve got the title and the amount.
//DataModel that defines what an food Item has as properties
struct Item: Codable {
var title: String = ""
var amount: Int = 0
}
To use it for decoding (retrieving data) and encoding (saving data), it must conform to the Codable protocol.
The next interesting thing is the coder class. Inside there we have two functions: one is for retrieving data and the other for saving it.
Let’s have a closer look at the function saveData().
Saving Data
func saveData() {
let encoder = PropertyListEncoder() do {
let data = try encoder.encode(foodArray)
try data.write(to: dataFilePath!)
} catch {
print("Error with saving data, \(error)")
}
}
- We create an instance called encoder from the class PropertyListEncoder().
- Then we encode the foodArray with encoder.encode() and store it into the variable data.
- After that, we save/write the data into the Item.plist file.
- The whole process must be done inside the do {} catch {}, because errors can always occur when saving data. The same is also true for retrieving data.
Now we saved the data in the Item.plist file. Let’s find out how we retrieve it.
Retrieving Data
func retrieveData() {
let decoder = PropertyListDecoder()
do {
let data = try Data(contentsOf: dataFilePath!)
foodArray = try decoder.decode([Item].self, from: data)
} catch {
print("Error with getting data, \(error)")
}
print("Food Items: \(foodArray)")
}
- Similar to the above we first create an instance called decoder from the class PropertyListDecoder().
- Next, we store the encoded data from Item.plist inside the variable data.
- Then we call the method decoder.decode([Item].self, from: data), that converts the information inside data into the datatype [Item]. The output of this method is stored inside the foodArray.
By the way, if you want to open the Item.plist file, copy the file path that is printed into the console (the path starts with Users/…). Then open the terminal, write “cd ..” and hit enter. Do it once again. Now you’re at the root. Next, write “open” and paste the file path into the terminal. This will open the .plist file, where all the food items are stored.
Conclusion
User Defaults is an easy and handy way of storing small bits of data like the score and player’s nickname. Be sure that you can only use the standard datatypes: Bool, String, Data, Float, Double, Int, Array, and Dictionaries.
If you want to use your own data models you have to use Codable. But also here, only use it for a small amount of data, because the program must load the whole .plist file before it can access the data inside there, even if it’s only a piece of small information that you need. I recommend not exceeding 1 KB.
To handle a bigger amount of data I recommend you to have a look at Core Data and Realm.
Thank you for reading!