It is finally time to learn how to save our data! Great! This was long overdue!
iOS makes it a breeze to save data! Well… at least for the users, because some great developers spent their time finding a way to make them forget about having to do so!
I still remember the dreadful days as a Windows user (or when I use Office for Mac, even the 2019 version, or any other app for Mac not natively developed on Mac) when I was quitting a program and getting the notification that if I had not saved my progress things would have been lost!
I’m trying something new myself here: in Xcode, I am branching from the master branch to a new branch called “project12a”.
Hopefully all my changes will be saved in the new branch and when I will want to go back I will just have to switch to the “master” one.
Completely new to this so… wish me luck!
Reading and writing basics:
This first part was simply about getting to know the
UserDefaults and its workings. This is a subclass of
NSObject described as “an interface to the user’s defaults database, where you store key-value pairs persistently across launches of your app”. This part from the Documentation’s Discussion seems important to me:
At runtime, you use
UserDefaultsobjects to read the defaults that your app uses from a user’s defaults database.
UserDefaultscaches the information to avoid having to open the user’s defaults database each time you need a default value. When you set a default value, it’s changed synchronously within your process, and asynchronously to persistent storage and other processes.
.standard class variable returns the shared defaults object and, if it doesn’t yet exist, it creates one. We can then store all this in a property and use it to write some sets of preferences using the
.set(_ value: Any?, forKey defaultName: String) method.
If we want to read preferences, instead, we need to use the
.objectType(forKey:) method, filling objectType with anything we may be looking for, from
String to whatever else.
Directly using the
.object(forKey:) creates some troubles because we receive an optional value in return and we need to downcast it and, possibly to nil cohalesce it to a default value in case it may not exist.
Fixing Project 10:
Let’s make some more magic now: we learn that we can save just any kind of data into
UserDefaults, as long as we follow some rules. In short:
- we use the
NSKeyedArchiverclass which is defined as “A coder that stores an object’s data to an archive referenced by keys”.
- we call its
archiveDatamethod which, strangely, is described as deprecated in the documentation… but what is taking its place? Whatever, this transforms an object graph (i.e.: that object plus all its references) into a
Dataobject and writes it to
- we call its
- data can be of any type as long as it is one of the simpler types or if it conforms to the
So, in project 10, we add conformance to the
NSCoding protocol at the top of the
Person class. We are explained that structs cannot conform to
NSCoding… I am not sure I understand this, but maybe I don’t have to. I will just tell my brain that “if I want to save something I have to make that something a class that inherits from
NSObject and that conforms to
Protocol conformance brings with it its issues and so we need to add the
encode method which will encode (i.e.: save) our name and image behind properly named keys (it looks like a dictionary and maybe it just is) and a
required init—that is an initialiser that subclasses will need to add as well if they inherit from this class—with an assignment to our
image properties to a call to the
decodeObject(forKey:) method. We also decide to play it safe and add a nil coalescing operation in case the data is not found.
the initialiser is used when loading objects of this class and
encode()is used when saving
Back in ViewController.swift we now have to write the actual code that saves and loads but we need
Data and we have arrays so… off… we need to convert things first.
Luckily for us we “just” need to add a new
save() method where we optionally try to store the return result of the call to
NSKeyedArchiver.archivedData on our
people’s array into an optionally bound constant and, if that succeeds, we will set that to be saved in our user defaults storage! Easy right? …yeah…!
Now we need to call
save() wherever we were reloading our collection-view data and, finally, to allow for loading in
viewDidLoad() so that when we launch the app the next time we will actually load the same data as the last time.
viewDidLoad(), we optionally bind the return object of the
defaults.object(forKey:)—using the same key we used to save the data, of course—to a constant and optionally downcast it to a
Data object. If this succeeds we optionally try to store the result of the call to
NSKeyedUnarchiver.unarchiveTopLevelObjectWithData()—did you try to say it in one breath— optionally downcast as an array of
Persons in a constant and, if also this succeeds we set our
people variable to this last constant! If, God forbid, anything would go wrong, we just load an empty
Fixing Project 10:
If we are only writing Swift,
Codable is the way to go. The differences between
NSCoding are the following:
Codableworks on both classes and structs.
- No need to write
- Native support for JSON read/write.
Not bad, right?
First, I checked-out to my
master branch and created a new branch from it called
project12a (starting to feel dangerous power flowing through my veins!) and added conformance to
Codable for the
Person class. And actually there’s nothing else here to do…! Amazing!
Back in ViewController.swift we create a
save() method. Inside, we declare and initialise a new
JSONEncoder(), optionally try to (optionally) bind the result of the call to that encoder’s
encode method to a constant and, if that succeeds, we set the saved data for the appropriate key in our user-defaults standard storage. Just for safety, if something goes wrong, we print a message to the console.
As before, we call this
save() method everywhere we reload our data.
We then move to
viewDidLoad(), create a call to our
UserDefaults.standard path, optionally bind the optional downcast to
Data result of the call of the
object(forKey:) method on our “people” key and, if that succeeds—breath!…—we declare a new
JSONDecoder() and set up a
do-catch block to try to create an array of
Person objects from the data extracted before.
This very last passage deserves a little clarification: the
object(forKey:) method pulls out optional
Data which gets unwrapped with
if let and
JSONDecoder then converts it back to an object graph—that is, our
That’s it! You can find the code for all the three versions of the project here.
Please let me know if something is amiss or suspiciously wrong.
Please don’t forget to drop a hello and a thank you to Paul for all his great work (you can find him on Twitter) and don’t forget to visit the 100 Days Of Swift initiative page.
If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).
If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).
You can also support me by buying Paul Hudson’s books from this Affiliate Link.
Anyways, thank you so much for reading!
Till the next one!