Application Architecture for SwiftUI & Firebase
Learn how to easily map Firestore data in Swift
Last time, we looked at how to connect a SwiftUI app to a Firebase project and synchronise data in real time. If you take a look at the code we used for mapping from Firestore documents to our Swift model structs, you will notice that is has a number of issues:
- It is rather verbose — even for just the few attributes we’ve got!
- The code makes some assumptions about the structure of the documents, such as the attribute types
- Things might start breaking if we change the structure of the documents or the struct holding our model
So in this post, I’m going to show you how to make your mapping code more concise, less error-prone, and more maintainable by using the Codable protocol.
What is Codable?
The Codable protocol is Apple's contribution to a standardised approach to encoding and decoding data. If you've been a Swift developer for a while, you might recall that, before Apple introduced Codable in Swift 4, you had to perform data mapping from and to external representations either manually or by importing third-party libraries. None of this is required anymore, thanks to Codable.
For a long time, Firestore lacked support for Codable, and the GitHub issue asking for adding support for object serialization in Firestore (#627) might be one of the most popular / upvoted issues of Firebase iOS SDK so far.
The good news is that, as of October 2019, Firestore provides support for Codable, and it's every bit as easy to use as you might hope it is. It essentially boils down to three steps:
- Add Firestore Codable to your project
- Make your models Codable
- Use the new methods to retrieve / store data
- (Bonus!) delete all of your existing mapping code
Let’s look at these a little bit closer.
Prepare your project
As Firestore’s Codable support is only available for Swift, it lives in a separate Pod, FirebaseFirestoreSwift - add this to your Podfile and run pod install.
Make your models Codable
Import FirebaseFirestoreCodable in the file(s) holding your model structs, and implement Codable, like so:
As we want to use the Book models in a ListView, they need to implement Identifiable, i.e. they need to have an id attribute. If you've worked with Firestore before, you know that each Firestore document has a unique document ID, so we can use that and map it to the id attribute. To make this easier, FirebaseFirestoreSwift provides a property wrapper, @DocumentID, which tells the Firestore SDK to perform this mapping for us.
If the attribute names on your Firestore documents match with the property names of your model structs, you’re done now.
However, if the attribute names differ, as they do in our example, you need to provide instructions for the Encoder / Decoder to map them correctly. We can do so by providing a nested enumeration that conforms to the CodingKey protocol.
In our example, the name of the attribute that contains the number of pages of a book is called pages in our Firestore documents, but numberOfPages in our Book struct. Let's use the CodingKeys to map them to each other:
It is important to note that once you use CodingKeys you'll have to explicitly provide the names of all attributes you want to map. So if you forget to map the id attribute, the ids of your model instances will be nil. This will result in unexpected behaviour, for example when trying to display them in a ListView. Check out Apple's documentation for a more detailed discussion of Codable.
Now that our model is prepared for mapping, we can update the existing mapping code on our view model. At the moment, it looks like this:
Time to delete some code and simplify this!
We can replace the mapping code in the documents.map closure with a much simpler version:
As you can see, we got rid of the entire process of manually reading attributes from the data dictionary, performing typecasts, and providing default values. Our code is a lot safer now that the SDK takes care of this for us.
All of this is thanks to the data(as:) method, which is provided by the FirebaseFirestoreSwift module. Doesn't it feel great to remove all this code?
So we’ve covered mapping data when reading it from Firestore — what about the opposite direction?
It turns out that this is almost as simple as reading data — let’s take a quick look. To write data, we can use use addDocument(from:) instead of .addDocument(data:):
This saves us from writing the mapping code, which means a lot less typing, and — more importantly — a lot less opportunity to get things wrong and introduce bugs. Great!
With the basics under our belt, let’s take a look at a couple of more advanced features.
We already used the @DocumentID property wrapper to tell Firestore to map the document IDs to the id attribute in our Book struct. There are two other property wrappers you might find useful: @ExplicitNull and @ServerTimestamp.
If an attribute is marked as @ExplicitNull, Firestore will write the attribute into the target document with a null value. If you save a document with an optional attribute that is nil to Firestore and it is not marked as @ExplicitNull, Firestore will just omit it.
@ServerTimestamp is useful if you want need to handle timestamps in your app. In any distributed system, chances are that the clocks on the individual systems are not completely in sync all of the time. You might think this is not a big deal, but imagine the implications of a clock running slightly out of sync for a stock trade system: even a millisecond deviation might result in a difference of millions of dollars when executing a trade. Firestore handles attributes marked with @ServerTiemstamp as follows: if the attribute is nil when you store it (using addDocument, for example), Firestore will populate the field with the current server timestamp at the time of writing it into the database. If the field is not nil when you call addDocument() or updateData(), Firestore will leave the attribute value untouched. This way, it is easy to implement fields like createdAt and lastUpdatedAt.
Where to go from here
Today, you learned how to simplify your data mapping code by using the Codable protocol with Firestore. Next time, we're going to take a closer look at saving, updating, and deleting data.
Thanks for reading, and make sure to check out the Firebase channel on YouTube.
Originally published at https://peterfriese.dev/swiftui-firebase-codable/.