SwiftUI: Fetching Data from Firestore in Real Time
Application Architecture for SwiftUI & Firebase
SwiftUI is an exciting new way to build UIs for iOS and Apple’s other platforms. By using a declarative syntax, it allows developers to separate concerns and focus on what’s relevant.
As with everything new, it sometimes takes a while to get used to how things are done in the new world. I often see people try to cram too many things into their SwiftUI views, resulting in less readable code that doesn’t work well.
It’s time to shake off the old habits of building MVCs (massive view controllers), folks!
In this blog post I’m going to show you a clean way to connect your SwiftUI app to Firebase and fetch data from Cloud Firestore.
The sample app
To get us started, consider the following app which displays a list of books:
Hard to believe, but in less than 40 lines of code, we’re able to not only define a simple data model for books, as well as some sample data, but also a screen that displays the books in a list - that’s the power of SwiftUI!
To fetch data from Firestore, you’ll first have to connect your app to Firebase. I’m not going to go into great detail about this here - if you’re new to Firebase (or your background is in Android or web development), check out this video which will walk you through the process (don’t worry, it’s not very complicated).
How to store our books in Firestore
Data in Firestore is stored in documents that contain a number of attributes. Documents are stored in collections. You cannot nest collections, but a document can contain collections, making it possible to create hierarchical data structures.
For our application, we will just use a simple collection named
books, which contains a number of documents that will represent our books. Each document will be made up of the following attributes:
Fetching data and subscribing to updates
Firestore does support one-time fetch queries, but it really shines when you use its realtime synchronisation to update data on any connected device. No longer will you have to implement pull-to-refresh - all data is kept in sync on all devices all of the time! Even better: the Firestore SDKs provide offline support out of the box, so all changes that the user made while their device was offline will automatically be synced back to Cloud Firestore once the device comes back online again.
Implementing all of this isn’t even particularly complicated - quite the opposite, as you will see. Offline support is enabled by default, and implementing real-time sync is a matter of registering a snapshot listener to a Firestore collection you’re interested in.
The boilerplate for registering a snapshot listener for the books collection in our sample app looks like this:
But where’s a good place to put the snapshot listener? After everything I told you about massive view controllers, you’re probably guessing it won’t be in the view itself - and you’re absolutely right!
Implementing the view model
A good way to keep our views clean and lean is to use an MVVM (Model, View, View Model) architecture. We’ve already got the view (
BooksListView) and model (
Book), so all that’s missing is the view model.
The primary responsibility of the view model is to provide access to the data we want to display in our UI. This is typically done in a view-specific way: we might want to display the list of books in two different ways: a list view and a cover flow. The list might display more details about each book (such as number of pages as well as the author’s name), whereas the cover flow would display gorgeous book covers, with just the book title. Both views need different attributes from the model, so the view models would expose different attributes (and the view model for the cover flow might also include code to fetch large book cover art).
A secondary responsibility of the view model is the provisioning of the data. In our simple application, we will handle data access directly in the view model, but in a more complex application I’d definitely recommend extracting this into a store or repository to make data access reusable across multiple view models.
To be able to use the view model in a SwiftUI view and subscribe to the data it manages, it needs to implement the
ObservableObject protocol, and all properties that we want our UI to be able to listen to need to be flagged using the
@Published property wrapper.
Here is the boilerplate for our view model:
With this in place, we can now add the code for registering the snapshot listener, and map the documents we receive into
Using the ViewModel in a SwiftUI view
Back in the BooksListView, we need to make two changes to use
BooksViewModel instead of the static list of books:
By using the
@ObservedObject property wrapper (1), we tell SwiftUI to subscribe to the view model and invalidate (and re-render) the view whenever the observed object changes. And finally, we can connect the List view to the
books property on the view model (2), and get rid of the local
book array. Once the view appears, we can tell the view model to subscribe to the collection. Any changes that the user (and anyone else) makes to the
books collection in Firestore will now be reflected in the app’s UI in realtime.
With just a few lines of code, we’ve managed to connect an existing app to Firebase, subscribe to a collection of documents in Firestore and display any updates in real time.
The key for a clean architecture is to extract the data access logic into a dedicated view model, and harness SwiftUI’s and Combine’s power to drive UI updates effortlessly.
Where to go from here
Interested in seeing how to build a real world application using Firestore and SwiftUI? Check out my multi-part blog post series in which I rebuild the iOS Reminders app using SwiftUI and Firebase:
- Replicating the iOS Reminders App using SwiftUI, Combine, and Firebase (Part 1)
- Replicating the iOS Reminders App, Part 2: Connecting SwiftUI and Cloud Firestore
- Replicating the iOS Reminders App, Part 3: Sign in with Apple using SwiftUI and Firebase