- Published on
Data Persistence
- Authors
- Name
- Misiel Rodriguez
- @misiel
Data persistence aka having a saved state of user's data through different app sessions, is a crucial component of many apps. Let's say we were making a habit tracking app - without data persistence we wouldn't be able to save the user's streaks from previous days. Or say we have a todo-list app, we would want our todo-list items to still be there when we come back to mark them off!
Like Room in Android development, CoreData is a library that sits on top of an SQL(lite) database and mainly used for storing large pieces of data for offline usage or for local cache. On a lesser extent to store smaller pieces of data or simple key-value pairs, like Android's SharedPreferences, iOS has UserDefaults. For simplicity sake and just to learn the concept of data persistence, I opted for UserDefaults in my app to store a list of saved words:
UserDefaults Initial Approach
The documentation for UserDefaults makes it fairly simple to use. To store data you would call: UserDefaults.standard.set(yourData, "yourKey")
and to retrieve data you would call: UserDefaults.standard.yourRetrievedDataType(forKey: "yourKey")
. Being that words can have multiple definitions and the API returns each as a separate word, my initial approach to storing them was to use a [String:[Word]] dictionary where the key was the word and the value was an array of the associated Word
objects. In theory, the dictionary would look like this:
"Word1" -> Word1A, Word1B, Word1C,
"Word2" -> Word2A
"Word3" -> Word3A, Word3B
I ended up implementing this approach but ran into some unforseen problems.
I couldn't just store a dictionary into UserDefaults that used a custom type.
UserDefaults only stores the following types which are called property list types:
- URL - Any (NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary) - Bool - Double - Float - Int - String
Word
was not a valid type. I thought maybe I could wrap it in aNSData(value: word)
typing but met issues with that as well.My
Word
objects were not saving properly.When "saving" a word through the favorite button, the word would get stored to the dictionary, but if I were to refetch that word through the API, it would be treated as a whole new
Word
object and its saved state would not persist - meaning aWord
with the same properties would be stored in the dictionary but UI wise, the word was not saved.In other words, I gave
Word
anisSaved
boolean property which would update when the user tapped the favorite button on theWordDetailVC
. Tapping the button saves the word to the dictionary and untapping it will remove it. I could fetch a word, save it, refetch the word through another search and that same word would haveisSaved = false
.
UserDefaults Refined Approach
Through some research I found that it is possible to save custom types to UserDefaults, I just had to use JSONEncoder/Decoder and make my word Word
struct conform to the Codable
protocol. I believe through encoding your type, UserDefaults treats it as a Data
object which is a valid type for it.
Sweet so I can now encode my dictionary and store it, but my Word objects are still not retaining their saved state. What gives?
Through some more debugging I realized one of my crucial mistakes and learned a lesson in Swift's heavy emphasis on Structs vs Classes. Structs are a value type and Classes are a reference type - copies of a struct are their own unique instances, copies of a class are pointing to the same instance.
So when I would change the isSaved
value of my Word struct, but then refetch that word and create a new instance of it, the value would resort back to its default initialization of being false. To fix this, I changed Word
from a struct to a class. I'm guessing Structs auto-implement the Hashable
protocol because after doing that change, Xcode prompted me to implement Hashable which leads to the following methods being written:
static func == (lhs: Word, rhs: Word) -> Bool {
return lhs.definition == rhs.definition
}
func hash(into hasher: inout Hasher) {
hasher.combine(mainWord)
hasher.combine(definition)
}
==
is a method that tells your code "this is how an instance of this class should be compared to other instances of this class". To keep it simple I made it so that two Words are equal if they have the same definition.
hash
is necessary when using dictionaries or sets as these data structures need hashes to know where to store your values. To give Word a unique hash, I just based it on the definition and the word itself. Speaking of sets, now that I had a custom equator and hasher after changing my struct to a class, I figured it'd be much simpler to use UserDefaults to store a Set of Words. This way UserDefaults can be the source of truth for isSaved
as each stored word is a unique value due to implementing the Hashable protocol.
With all of these changes, UserDefaults was now ready to store my list of favorited words!
Conclusion
Through adding data persistence to my app, I ended up learning several things along with it:
- structs v classes (value vs reference)
- property list types
- JSONEncoder/Decoder
- UserDefaults v CoreData
This was one of my favorite (pun intended) features to add and also taught me a bunch that I will carry on through my iOS dev journey.
Thanks for reading!