Reading Firestore Data

Complete guide to reading and querying data from Cloud Firestore in iOS apps, including real-time listeners and data modeling

Reading Firestore Data

Note:

This tutorial covers reading, querying, and listening for real-time updates from Cloud Firestore. Reading collections from the Firestore database is a very simple process.

Overview

Reading collections from the Firestore database is a straightforward process. In this guide, we'll start with the basics of fetching data and move on to more advanced topics like real-time listeners and complex queries.

Reference:

Step 1: Basic Data Fetching

First, as with any service, start by binding Firestore to a variable within a function.

import Firebase
import FirebaseFirestore

class FeedViewController: UIViewController {

    private let db = Firestore.firestore()

    override func viewDidLoad() {
        super.viewDidLoad()
        fetchPosts()
    }

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

You can listen to the IDs in the desired collection with .addSnapshotListener. If no error is encountered during this listening and the IDs are not empty, these IDs will correspond to a document. You can get the fields you want within the collection with the .get command and transfer them to an array you have added.

Here is a more complete example:

func fetchPostsWithDetails() {
    db.collection("posts").addSnapshotListener { (querySnapshot, error) in
        guard let documents = querySnapshot?.documents else {
            print("No documents")
            return
        }

        let posts = documents.map { queryDocumentSnapshot -> Post in
            let data = queryDocumentSnapshot.data()
            let author = data["author"] as? String ?? ""
            let text = data["text"] as? String ?? ""
            return Post(author: author, text: text)
        }
        
        // Now you have an array of Post objects to use in your UI
        self.displayPosts(posts)
    }
}

Step 2: Real-time Listeners

For real-time updates, you can use addSnapshotListener. This will notify your app of any changes in the collection.

func listenForRealTimeUpdates() {
    db.collection("posts")
      .order(by: "timestamp", descending: true)
      .addSnapshotListener { querySnapshot, error in
        guard let snapshot = querySnapshot else {
            print("Error fetching snapshots: \(error!)")
            return
        }
        snapshot.documentChanges.forEach { diff in
            if (diff.type == .added) {
                print("New post: \(diff.document.data())")
                // Handle new post
            }
            if (diff.type == .modified) {
                print("Modified post: \(diff.document.data())")
                // Handle modified post
            }
            if (diff.type == .removed) {
                print("Removed post: \(diff.document.data())")
                // Handle removed post
            }
        }
    }
}

Step 3: Querying and Filtering Data

Firestore allows for powerful querying and filtering of your data.

Simple Queries

func fetchUserPosts(userId: String) {
    db.collection("posts")
      .whereField("authorId", isEqualTo: userId)
      .getDocuments { (querySnapshot, error) in
        // ... handle results
    }
}

Complex Queries

You can chain multiple whereField clauses to create more complex queries. You may need to create a composite index in the Firebase Console for these queries.

func fetchPopularPosts(inCategory category: String) {
    db.collection("posts")
      .whereField("category", isEqualTo: category)
      .whereField("likes", isGreaterThan: 100)
      .order(by: "likes", descending: true)
      .limit(to: 20)
      .getDocuments { (querySnapshot, error) in
        // ... handle results
    }
}

Step 4: Data Modeling with Codable

For cleaner code, you can use Swift's Codable protocol to map Firestore documents to your custom Swift types.

Create a Codable Model

import FirebaseFirestoreSwift

struct Post: Codable, Identifiable {
    @DocumentID var id: String? = UUID().uuidString
    var text: String
    var author: String
    var authorId: String
    var timestamp: Timestamp
    var likes: Int = 0
}

Fetch and Decode Data

func fetchAndDecodePosts() {
    db.collection("posts").getDocuments { (querySnapshot, error) in
        if let error = error {
            print("Error getting documents: \(error)")
        } else if let querySnapshot = querySnapshot {
            let posts = querySnapshot.documents.compactMap { document -> Post? in
                try? document.data(as: Post.self)
            }
            // You now have an array of [Post]
            self.displayPosts(posts)
        }
    }
}

Note:

Reading data from Firestore is a core feature for building dynamic apps. Leveraging real-time listeners and Codable can greatly simplify your codebase.