Firebase Realtime Database has been used since several years by mobile developers, for making robust apps easily without spending a lot of time and effort over a backend database and infrastructure. Firebase makes it very easy by providing ready-made solutions for data storage on the cloud and user authentication, management and security.

Recently, Google announced another backend solution called Firestore, built with better and greater features, the most significant of which are below.

Better data model. Firestore stores data in collections, which are similar to database tables. You can have several collections in your database, in contrast to Realtime database which stored all the data in one big JSON tree, which made it difficult for complex data structuring.
Each collection can have several ‘documents‘. Documents have an identifier and contain many key-value pairs; the values of which can be strings, number, boolean, arrays of these, or sub-collections.

Querying. Using Firestore you can make complex queries and get filtered data by multiple where clauses, as we shall see further in the tutorial. Realtime Database offered very limited querying and sorting capabilities. Firestore can also perform batch write operations.

Setting up your project

I will go over this very quickly, just summarizing the steps. If you have never used Firebase or need help creating Xcode projects, I recommend going through a detailed documentation first. A summary of the steps to create a new Firebase project.

  1. Create a new Xcode project. Copy the bundle identifier.
  2. Login with your Google account and go to Firebase Console.
  3. Add a new project, enter the name, click Create. Follow the steps Firebase takes you through.
  4. Enter the copied bundle identifier from your Xcode project.
  5. Download the plist configuration file and move it to your project root folder.

If you haven’t initialised Cocoapods for your project, do so by opening terminal, traversing to your project folder and running ‘pod init’.

Next, open podfile and add these pods

pod 'Firebase/Core' 
pod 'Firebase/Firestore'

and run

pod install

Go back to Firebase Console and open your app there. Select Database on the left pane and Create Database from the right pane.

Select Start in test mode, so that we will be able to perform read and write operations.

You will now be able to see your database. you can manually add some collections/documents to try.

Initialise Firebase for your app

Open your AppDelegate file and import Firebase.

import Firebase

Inside the application didFinishLaunchingWithOptions method, add this line.

FirebaseApp.configure()

Open the ViewController where you will be performing read/write operations to you Firestore database. Import the database.

import FirebaseFirestore

And create an instance of the database to be used.

var db: Firestore!
db = Firestore.firestore()

Adding data

Add a new document with a generated ID

db.collection("books").addDocument(data: [
        "title": "To kill a mockingbird",
        "author":"Harper Lee",
        "published": 1960 ]) 
    { err in
        if let err = err {
            print("Error adding document: \(err)")
        } else {
            print("Document added with ID: \(ref!.documentID)")
        }
    }

Add a new document with a specified ID

db.collection("cities").document("SF").setData([
    "name": "San Francisco",
    "state": "CA",
    "country": "USA",
    "capital": false,
    "population": 860000,
    "regions": ["west_coast", "norcal"]
])

Read all documents from a collection

db.collection("books")
    .getDocuments() { (querySnapshot, err) in
        if let err = err {
            print("Error getting documents: \(err)")
        } else {
            for document in querySnapshot!.documents {
                print("\(document.documentID) => \(document.data())")

            }
        }
    }

Read a particular document from a collection, by ID

db.collection("cities").document("SF")
    .getDocument { (document, error) in
        if let document = document, document.exists {
            let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
            print("Document data: \(dataDescription)")
        } else {
            print("Document does not exist")
        }
    }

Casting result to a custom object

First create a Codable struct of your object. For this simple example we have a VideoObj having just 2 fields, title and video_id.

struct VideoObj: Codable {
    var video_id: String
    var title: String
}

In your ViewController, create a JSONDecoder instance,

    let decoder = JSONDecoder()

And when you retrieve the data you can convert it to JSON and decode it.

db.collection("videos")
    getDocuments() { (querySnapshot, err) in
        if let err = err {
            print("Error getting documents: \(err)")
        } else {
            for document in querySnapshot!.documents {
                do {
                    let jsonData = try? JSONSerialization.data(withJSONObject:document.data())
                    let vid = try self.decoder.decode(VideoObj.self, from: jsonData!)
                    self.videosArray.add(vid)
                } catch let error  {
                    print(error.localizedDescription)
                }
            }
      }
}

Alternatively, you can use this awesome library, Codable Firebase.

Filtering Data

You can perform simple and compound queries on the Firestore database. The query is equivalent to an SQL where clause. For example, the below code will return all documents that have the given field value as true.

db.collection("products").whereField("instock", isEqualTo: true)
                .getDocuments() { (querySnapshot, err) in
                        if let err = err {
                                print("Error getting documents: \(err)")
                        } else {
                                for document in querySnapshot!.documents {
                                        print("\(document.documentID) => \(document.data())")
                                }
                        }
        }

Some other examples of single where clauses.

citiesRef.whereField("instock", isEqualTo: true)
citiesRef.whereField("category", isEqualTo: "books")
citiesRef.whereField("price", isLessThan: 1000)
citiesRef.whereField("name", isGreaterThanOrEqualTo: "To kill a Mockingbird")
citiesRef.whereField("sizes", arrayContains: "small")

Compound queries

Firestore allows combining multiple where clauses, but with some limitations. From Firebase documentation:

You can also chain multiple where() methods to create more specific queries (logical AND). However, to combine the equality operator (==) with a range or array-contains clause (<, <=, >, >=, or array_contains), make sure to create a composite index.

You can only perform range comparisons (<, <=, >, >=) on a single field, and you can include at most one array_contains clause in a compound query:

Some examples of compound queries are:

citiesRef
    .whereField("state", isEqualTo: "CO")
    .whereField("name", isEqualTo: "Denver")

citiesRef
    .whereField("state", isEqualTo: "CA")
    .whereField("population", isLessThan: 1000000)
        
citiesRef
    .whereField("state", isGreaterThanOrEqualTo: "CA")
    .whereField("population", isGreaterThan: 1000000)

Order and limit

By default, queries return all the documents satisfied by the query, and this result is ordered in ascending order of the document ID.

But you can specify the order and limit of your result.

citiesRef.order(by: "name").limit(to: 3)

For descending order

citiesRef.order(by: "name", descending: true).limit(to: 3)

Ordering by multiple fields

citiesRef.order(by: "state")
      .order(by: "population", descending: true)

Again, when combining where and order, Firebase states:

However, if you have a filter with a range comparison (<, <=, >, >=), your first ordering must be on the same field.

citiesRef
     .whereField("population", isGreaterThan: 100000)
     .order(by: "population")

. . .