AAH
AAH

Reputation: 127

SwiftUI - Wait until Firestore getDocuments() is finished before moving on

I know there are a couple questions with similar structure but I am really struggling on how to make my code which calls the method that retrieves the Firestore data, wait until the asynchronous function is complete before returning.

At the moment I have the parent function which calls getPledgesInProgress, the method which retrieves the data from firebase. This method is called when the parent is initialised.

    @State var pledgesInProgress = [Pledge]()
    var pledgePicked: Pledge?

    var body: some View {
        VStack{
       
           //LOTS OF CODE HERE WHICH ISN'T RELEVANT
    }
    
    func initVars(){
        pledgesInProgress = getPledgesInProgress(pledgePicked: pledgePicked ?? emptyPledge)
    }
}

The issue is that the pledgesInProgress variable gets initialised with an empty array, because the parent doesn't wait until the called function is finished getting the Firestore documents before continuing.

func getPledgesInProgress(pledgePicked: Pledge)-> [Pledge]{
 
    let db = Firestore.firestore()
    var pledgesToReturn = [Pledge]() //INITIALISED AS EMPTY

  
   db.collection("Pledges")
      .getDocuments { (snapshot, error) in
         guard let snapshot = snapshot, error == nil else {
          //handle error
          return
        }
  
        snapshot.documents.forEach({ (documentSnapshot) in

          let documentData = documentSnapshot.data()
                pledgesToReturn.append(findPledgeWithThisID(ID: documentData["ID"] as! Int))
        })
      
      }

//PROBLEM!!!! returned before getDocuments() completed
return pledgedToReturn 
}

The issue is the the array pledgesToReturn is returned before the getDocuments() method is finished, and so it is returned as an empty array every time. Please can someone help me understand how to get the method to wait until this call has completed? Thanks

N.B Pledge is a custom data type but it doesn't matter, it's about understanding how to wait until the asynchronous function is completed. You can replace the Pledge data type with anything you like and it will still be the same principle

Upvotes: 4

Views: 3941

Answers (1)

Peter Friese
Peter Friese

Reputation: 7254

Firebaser here. First, please know most (!) Firebase APIs are asynchronous, and require you to use completion handlers to receive the results. This will become easier with the general availability of async/await, which will enable you to write straight-line code. I recorded a video about this a while ago - check it out to get an idea of how you will be able to use Firebase with Swift 5.5.

There are two ways to solve this: using completion handlers or using async/await. I will describe both below, but please note async/await is only available in Swift 5.5 and requires iOS 15, so you might want to opt for completion handlers if you're working on an app that you want to ship to users that use iOS 14.x and below.

Using completion handlers

This is the current way of handling results from Firebase APIs.

Here is an updated version of your code, making use of completion handlers:

func getPledgesInProgress(pledgePicked: Pledge, completionHandler: ([Pledges]) -> Void) {
 
  let db = Firestore.firestore()
  var pledgesToReturn = [Pledge]() //INITIALISED AS EMPTY

  
  db.collection("Pledges")
    .getDocuments { (snapshot, error) in
    guard let snapshot = snapshot, error == nil else {
      //handle error
      return
    }
  
    snapshot.documents.forEach({ (documentSnapshot) in
      let documentData = documentSnapshot.data()
      pledgesToReturn.append(findPledgeWithThisID(ID: documentData["ID"] as! Int))
    })

      // call the completion handler and pass the result array
      completionHandler(pledgesToReturn]
  }
}

And here is the view:


struct PledgesInProgress: View {
  @State var pledgesInProgress = [Pledge]()
  var pledgePicked: Pledge?

  var body: some View {
    VStack {       
      // LOTS OF CODE HERE WHICH ISN'T RELEVANT
    }
    .onAppear {
      getPledgesInProgress(pledgePicked: pledgePicked) { pledges in
        self.pledgesPicked = pledges
      }
    }
  }    
}

Using async/await (Swift 5.5)

Your original code it pretty close to the code for an async/await implementation. The main things to keep in mind are:

  1. mark all asynchronous functions as async
  2. call all asynchronous functions using await
  3. (if you're using view models) mark your view model as @MainActor
  4. to call async code from within SwiftUI, you can either use the task view modifier to execute your code when the view appears. Or, if your want to call from a button handler or another synchronous context, wrap the call inside async { await callYourAsyncFunction() }.

To learn more about this, check out my article Getting Started with async/await in SwiftUI (video coming soon). I've got an article about async/await and Firestore in the pipeline - once it goes live, I will update this answer.

func getPledgesInProgress(pledgePicked: Pledge) async -> [Pledges] {
 
  let db = Firestore.firestore()
  var pledgesToReturn = [Pledge]() //INITIALISED AS EMPTY

  let snapshot = try await db.collection("Pledges").getDocuments()
  snapshot.documents.forEach { documentSnapshot in
    let documentData = documentSnapshot.data()
    pledgesToReturn.append(findPledgeWithThisID(ID: documentData["ID"] as! Int))
  }

  // async/await allows you to return the result just like in a normal function:
  return pledgesToReturn
  }
}

And here is the view:


struct PledgesInProgress: View {
  @State var pledgesInProgress = [Pledge]()
  var pledgePicked: Pledge?

  var body: some View {
    VStack {       
      // LOTS OF CODE HERE WHICH ISN'T RELEVANT
    }
    .task {
      self.pledgesPicked = await getPledgesInProgress(pledgePicked: pledgePicked)
    }
  }    
}

A few general remarks

There are a couple of things to make your code more resilient:

  1. Consider using Codable to map your documents. See Mapping Firestore Data in Swift - The Comprehensive Guide, in which I explain how to map the most common data structures between Swift and Firestore.
  2. Please don't use the force unwrap operator - your app will crash if the unwrapped object is nil. In this instance, using Codable will help you to avoid using this operator, but in other cases you should consider using if let, guard let, optional unwrapping with and without default values. Check out Optionals In Swift: The Ultimate Guide – LearnAppMaking for more details.
  3. I would strongly recommend using view models to encapsulate your data access logic, especially when accessing Firestore or any other remote backend. Check out this code to see how this can be done.

Upvotes: 13

Related Questions