FirestoreQuery & SwiftUI: The easiest way to listen for real-time updates

Firebase has long been a place for developers to rapidly prototype a functional front-end that interfaces with a full-fledged enterprise level cloud system.

For us Swift developers, Firebase has supported us as the language has grown. Firebase was made available via Swift Package Manager recently, and there’s a lot of new APIs that came out for SwiftUI with that particular release. Today, I want to walk us through a few, but really one in particular: @FirestoreQuery(collection:) !!!

Prerequisites

I’m hoping this article is easily digestible if you know what you’re doing with Firebase. But if you’re on the steeper part of the learning curve — worry not. Let me outline the bare minimum required for getting a FirestoreQuery up and running.

  1. A Firebase project (Set up or login to an existing Firebase project in your Firebase Console).
  2. An Xcode project with an iOS, macOS, or tvOS app target. (FirestoreQuery — or Firestore for that matter — does not yet work on watchOS)
  3. The Firebase Apple SDK downloaded and installed into your project via Cocoapods or Swift Package Manager.
  4. Link the library FirebaseFirestoreSwift to your target’s linked libraries and frameworks.

See the Firebase documentation for more details on installation.
👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇

The API

FirebaseFirestoreSwift has some new APIs that make querying for your documents a breeze.

@FirestoreQuery

The new @FirestoreQuery(collection:) property wrapper is a pretty darn flexible little API. Here is a very simple example of how you can listen for a collection of documents, and display them in a list using SwiftUI:

import SwiftUI
import FirebaseFirestoreSwift
@main
struct FooBarApp: App {
var body: some Scene {
ContentView()
}
}
struct Foo: Codable, Identifiable {
@DocumentID var id: String?
@ServerTimestamp var created: Date?
var bar: String
}
struct ContentView: View {
@FirestoreQuery(collection: "foos") var allTheFoo: [Foo]
var body: some View {
List {
ForEach(allTheFoo) { foo in
Text(foo.bar)
}
}
}
}

If you run this code in your app you’ll… probably see nothing… if there’s nothing in your database, that is. Hopefully though you have an existing data model and BLAM, you’ve displayed like all of them in your list.

If you in fact don’t have any data in your database, leave your new app running in your simulator (or on device) and navigate to your Firestore dashboard in your Firebase project. Create a collection called foos and add a new object with an autogenerated id, and a string variable called bar.

Like magic, there should now be a new view displayed in your list!

@DocumentID

You might’ve noticed a property wrapper in the model code above:

@DocumentID var id: String?

DocumentID is a real easy way to tell firebase that, when this object is encoded to Firebase, if this variable is nil, create a new document in the given collection.

@ServerTimestamp

Another property wrapper that Firebase provides us is:

@ServerTimestamp var created: Date?

If this is left nil then Firebase will auto-generate a UTC timestamp based on the server’s clock. This variable also handles race conditions across multiple clients, which is an incredibly complex set of problems to handle.

For example: Imagine a chat app that is leveraging a Chat model object. Chat includes an array of Message objects called messages. As each user sends their message, we append the last message to Chat.messages and save that mew Message document with a new updated timestamp variable.

Normally, this would result in some unreliable race conditions. But, wrapping the updated variable within a @ServerTimestamp property wrapper will leverage Firebase to timestamp the documents in the order in which they were saved on the client side. That way, the chat is always in the order which the messages were sent, even when there is not a network connection. Pretty fancy!

Sometimes Google, you’re not so bad. (Most of the time though you’re straight up evil, so let’s not forget that.)

Predicates

Often times when we design apps with Firestore us savvy developers will rely on querying our data to show the user the data that they’re interested in. We need to filter our documents to get just the right amount of information to give our users the best UX possible. We also need to keep our queries to a minimum (cause that sh*t’s expensive!).

Luckily, @FirestoreQuery(collection:predicates:) is a thing!

import SwiftUI
import FirebaseFirestoreSwift
struct ContentView: View { @FirestoreQuery(
collection: "foos",
predicates: [ // <- This is new
.equalTo("bar", "meow")
]
) var allTheFoo: [Foo]
var body: some View {
List {
ForEach(allTheFoo) { foo in
Text(foo.bar)
}
}
}
}
}

Often times we need to dynamically add filters when the user clicks a button, or are based on a variable passed into the view. Well, the Firebase team has made this one pretty slick:

import SwiftUI
import FirebaseFirestoreSwift
struct ContentView: View { let state: String = "WA" @FirestoreQuery(collection: "foos") var allTheFoo: [Foo] var body: some View {
List {
ForEach(allTheFoo) { foo in
Text(foo.bar)
}
.onAppear {
$allTheFoo.predicates.append(.arrayContains("state", state)) // <- This can be invoked anywhere on the MainActor from within this view, like, for example, on a button press.
}
}
}
}

There are a bunch of different predicates that are viable to use:

  • isEqualTo(_ field: String, _ value: Any)
  • isIn(_ field: String, _ values: [Any])
  • isNotIn(_ field: String, _ values: [Any])
  • arrayContains(_ field: String, _ value: Any)
  • arrayContainsAny(_ field: String, _ values: [Any])
  • isLessThan(_ field: String, _ value: Any)
  • isGreaterThan(_ field: String, _ value: Any)
  • isLessThanOrEqualTo(_ field: String, _ value: Any)
  • isGreaterThanOrEqualTo(_ field: String, _ value: Any)
  • orderBy(_ field: String, _ value: Bool)
  • limitTo(_ value: Int)
  • limitToLast(_ value: Int)

Visit Google’s official documentation for more information on the intricacies of querying for your data:

Error Handling

The same way that we can access the predicates dynamically, we can also access an optional error to display or log errors as needed:

import SwiftUI
import FirebaseFirestoreSwift
struct ContentView: View {@FirestoreQuery(collection: "foos") var allTheFoo: [Foo]var body: some View {
List {
if $allTheFoo.error != nil {
Text("There was an error: " + fruits.error!.localizedDescription)
}
ForEach(allTheFoo) { foo in
Text(foo.bar)
}
}
}
}

Alternatively, you can cast the FirestoreQuery as a Result<[Foo], Error> and handle the switch case in the body to conditionally show an error:

import SwiftUI
import FirebaseFirestoreSwift
struct ContentView: View {@FirestoreQuery(collection: "foos") var allTheFoo: [Foo]var body: some View {
List {
if case let .success(allTheFoo) = fruitResults {
ForEach(allTheFoo) { foo in
Text(foo.bar)
}
} else if case let .failure(error) = fruitResults {
Text("Couldn't map data \(error.localizedDescription)")
}
}
}
}

Conclusion

@FirestoreQuery is an incredibly powerful little API that can. For real, it’s helped me do a ton in a very short amount of time.

If you have any questions or adjustments on how this works leave me a comment here.

I hope to be writing more, so please subscribe to my blog and I’ll see you in the next one!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store