Apekshik Panigrahi
Apekshik Panigrahi

Reputation: 206

Calling an async query which relies on another async call to firestore

I've been trying to implement a rudimentary follower feed system using firestore in my swift app. I've got firestore set up with three top level collections: Users, Reviews, and Comments. I wish to populate my feed with the most recent reviews that a particular set of users posted. To do this, I first need to fetch the set of users from the Users collection (the users I currently follow) and then use documentIDs from these (users I follow) to fetch the respective reviews from the Reviews collection (3 most recent reviews per user I follow).

However, since calls through the SDK are async, I'm struggling to fetch the users first and THEN the reviews for my feed. I'm pretty new to async and await though I've gained a somewhat thorough understanding of concurrency in swift. Can someone point me in the right direction?

The code for fetching the users is as follows:

  private func fetchReviews(for userID: String) {
    let reviewRef = FirebaseManager.shared.firestore.collection("Reviews")
    
    reviewRef
      .whereField("uid", isEqualTo: userID)
      .order(by: "createdAt", descending: true)
      .limit(to: 3)
      .getDocuments { querySnapshot, error in
        guard let documents = querySnapshot?.documents, error == nil else { return }
        
        reviews = documents.compactMap({ queryDocumentSnapshot in
          try? queryDocumentSnapshot.data(as: Review.self)
        })
      }
  }

If I define a function to fetch the users I currently follow:

  // Fetches all users I follow and stores it in the usersIFollow var.
  private func fetchUsersIFollow() {

    // fetch the "following" subcollection within the current user's document.
    guard let userID = FirebaseManager.shared.auth.currentUser?.uid else { return }
    let db = FirebaseManager.shared.firestore
    db.collection("Users").document(userID).collection("Following").getDocuments { querySnapshot, error in
      guard let documents = querySnapshot?.documents, error == nil else { return }

      usersIFollow = documents.compactMap { queryDocumentSnapshot in
        try? queryDocumentSnapshot.data(as: User.self)
      }
    }
  }

(where usersIFollow: [User] is a state variables) and then call it in the fetchReviews() method like so

  private func fetchReviews(for userID: String) {
    fetchUsersIFollow() 
    let reviewRef = FirebaseManager.shared.firestore.collection("Reviews")
    
    reviewRef
      .whereField("uid", isEqualTo: userID)
      .order(by: "createdAt", descending: true)
      .limit(to: 3)
      .getDocuments { querySnapshot, error in
        guard let documents = querySnapshot?.documents, error == nil else { return }
        
        reviews = documents.compactMap({ queryDocumentSnapshot in
          try? queryDocumentSnapshot.data(as: Review.self)
        })
      }
  }

it doesn't work since both are async calls. How do I tweak this?

Note: I know this isn't probably the best way of handling a feed system. It's just a basic implementation which I'll further alter as I develop my app.

Upvotes: 0

Views: 285

Answers (2)

Rohit Kharche
Rohit Kharche

Reputation: 2919

I think you can use closures to fetch the users that you follow first, and then use their document IDs to fetch their reviews,

private func fetchFollowers(completion: @escaping ([String]) -> Void) {
  let followersRef = FirebaseManager.shared.firestore.collection("Users")
  
  followersRef
    .whereField("uid", isEqualTo: userID)
    .getDocuments { querySnapshot, error in
      guard let documents = querySnapshot?.documents, error == nil else { return }
      
      let followerIDs = documents.compactMap({ queryDocumentSnapshot in
        return queryDocumentSnapshot.documentID
      })
      
      completion(followerIDs)
    }
}

//your fetchReviews function will go here as it
private func fetchFeed() {
  fetchFollowers { followerIDs in
    for followerID in followerIDs {
      fetchReviews(for: followerID)
    }
  }
}

This way, you can fetch the users that you follow first, and then use their document IDs to fetch their reviews.

Upvotes: 1

Josh R
Josh R

Reputation: 111

There's two approaches that you can take:

  1. Call fetchReviews in the getDocuments closure in fetchFollowers.
  2. Use the async version of getDocuments, make fetchFollowers and fetchReviews async, and then call them from within a Task block in the right order. Task is used to call asynchronous code from within synchronous code by running it on a separate thread.

Pseudocode for (1) the closure approach:

fetchFollowerReviews() {
   configure followersRef

   followersRef.getDocuments { 
      followers = safely unwrapped snapshot

      configure reviewsRef using followers ids
      reviewsRef.getDocuments {
         reviews = safely unwrapped snapshot
         handle reviews data
      }
   }
}

Pseudocode for (2) the async approach:

fetchFollowerReviews() {
   Task {   
      let followers = try await fetchFollowers()
      let reviews = try await fetchReviews(from: followers)
      handle reviews
   }
}

fetchFollowers() async throws -> [Follower] {
   configure docRef
   let snapshot = try await docRef.getDocuments()
   followers = safely unwrapped snapshot
   return followers
}

fetchReviews(from: followers) async throws -> [Reviews] {
   configure docRef using followers ids
   let snapshot = try await docRef.getDocuments()
   reviews = safely unwrapped snapshot
   return reviews
}

Upvotes: 2

Related Questions