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 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
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
Codableto your project
- Make your models
- 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
Make your models Codable
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
Decoder to map them correctly. We can do so by providing a nested enumeration that conforms to the
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
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
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:
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
updateData(), Firestore will leave the attribute value untouched. This way, it is easy to implement fields like
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/.