Whenever data is created in a Swift app, it is loaded into memory. However, memory is temporary storage and if the user ever closes your app (which will eventually happen) that data will be cleared to make space for another app that needs that space in memory for its data. Therefore we need some sort of process for storing data in a way that allows it to persist any different life cycles of an application.
This is where the idea of Persistent Data is valuable. To make data persist in an app, it needs to be written somewhere in a device’s directory. This is the same as when you create a file on your laptop and save it in your Documents folder. That way when you restart your computer it will still be there!
Swift makes this process very easy by providing utils that handle the storage and encoding/decoding of our apps. It is much simpler than managing a SQL database and allows developers to save and load their data models on the fly!
Documents Directory
iOS gives developers space to store data for their apps. In order to gain a reference to this directory in the swift code it is useful to create a method that you can call to get this path.
The first line below creates an array of URL objects. It does this by using the FileManager object to retrieve urls by using the .urls(for:, in:) method where for: is the directory you would like to look for and in: is the domain in which to look for. We would like to look for the Documents directory in the User domain so the code below will suffice.
We then return the first element of the array which contains a URL reference to the Documents directory.
This second method appends a filename to the end of our directory to save our data in.
This method will return the full path with the FileName.plist file appended at the end of it. We will use this dataFilePath() method for reference when we are saving and loading data in our app.
.plist files
.plist files are a filetype that iOS uses to store information about an app. It is an XML file that stores information in the form of a Dictionary which uses key/value pairings. You might have noticed that when creating a Swift application in XCode, you are given an info.plist file. This is another .plist file that holds information about your app such as supported orientations of your app and the bundle name associated with your app. For the purposes of saving data in an app we are using a .plist file to save Swift objects.
Saving Data
We will be saving an array of a data type Item to the .plist file.
It is nice to create a handy method like saveItems() that can be called whenever we need to save data.
In order to save our data, it first needs to be encoded. To encode the data, we have to create an NSKeyedArchiver object. This archiver requires an NSMutableData() object to write the encoding to.
The next step is the actual encoding. We simply call the .encode(forKey:) method on the archiver which takes the data that you would like to encode and a key. This key is used to identify the object when we need to load it back later. Then just call finishEncoding(). After the encoding we can easily call the .write(to:, atomically:) method on our data object where :to requires a path to where we would like to write our data, for this we can call our handy dataFilePath() method. The parameter atomically: is set to true which writes our data to a backup location.
Loading Data
Loading data from the .plist file is very easy. We will create another handy method for loading data.
We first get a reference to the file path of our .plist file by calling our dataFilePath() method.
Then the try? tries to create a new Data object, if it cannot then it will return nil, resulting in the block of code not executing. Creating the Data object returns nil if the path we gave it does not exist. (Swift forces us to be cautious of situations like this with optionals) If we are able to create that Data object then we create a NSKeyedUnarchiver to decode our stored object and give it our data object as a constructor parameter. Note that when we call the .decodeObject() method we give it the key we used to store our object earlier. Also, we must cast the returned value to our data type by using the as! (forced optional unwrap) operator since Swift cannot infer what the type of the encoded object should be. The resulting value of the decoding is stored back in our global array of Items. Lastly we call .finishDecoding().
Using the Save/Load Methods
These methods are super handy because we can throw then anywhere in our app! If the values of our data are modified we can simply call saveItems() and when that app is starting up we can call loadItems(). Also, any time that our app is interrupted (which happens all the time) we can call a save to make sure no data is lost!