0

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.

1

2 Answers 2

2

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 } 
Sign up to request clarification or add additional context in comments.

7 Comments

I tried something very similar to the async approach but instead of returning data directly from each method, I updated the state variables with an assignment directly inside each method. Why does this solution sequentially return followers first and then reviews next even though they're async?
@ApekshikPanigrahi how were you calling the fetchFollowers and fetchReviews functions in your previous attempt? Can you share some sample code or pseudocode? In the example I shared, the Task block is executed sequentially and the await keyword suspends execution until the function completes, therefore ensuring that they execute in order. My hunch is that your previous approach didn't work because the state variable wasn't updated on the main thread (e.g. stackoverflow.com/questions/68026622/…)
In any scenario, you should ensure that you are dispatching updates to the state variable on the main thread by using the @MainActor modifier or DispatchQueue.main.async { } block.
It depends on how the async methods are dispatched and from where. This table is pretty useful to understand what context, if any, is inherited from the actor that runs the task (developer.apple.com/videos/play/wwdc2021/10134/?time=1582). Looks like the Task { } block inherits actor context from where it was called. So if you call Task { } from the MainActor, it will then execute synchronous functions on the MainActor. When the Task { } is called from a UIView or UIViewController (or View in SwiftUI), it will run on the MainActor / main thread, because those classes run on main.
This link explains a little more about the MainActor inheritance: stackoverflow.com/questions/67954414/…
|
1

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.

1 Comment

Your provided solution is correct (found nothing wrong with it!), but @John R's solution contains both the closure method as well as the new swift async/await method, so I accepted that as my answer :) Thank you for the response!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.