0

I have some code that reads data from Firebase on a custom loading screen that I only want to segue once all of the data in the collection has been read (I know beforehand that there won't be more than 10 or 15 data entries to read, and I'm checking to make sure the user has an internet connection). I have a loading animation I'd like to implement that is started by calling activityIndicatorView.startAnimating() and stopped by calling activityIndicatorView.stopAnimating(). I'm not sure where to place these or the perform segue function in relation to the data retrieval function. Any help is appreciated!

let db = Firestore.firestore()
            
    db.collection("Packages").getDocuments{(snapshot, error) in
        
        if error != nil{
            // DB error
        } else{
            
            for doc in snapshot!.documents{
                
                self.packageIDS.append(doc.documentID)
                self.packageNames.append(doc.get("title") as! String)
                self.packageIMGIDS.append(doc.get("imgID") as! String)
                self.packageRadii.append(doc.get("radius") as! String)

            }
            
        }
        
    }
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Use the completion block. – El Tomato Jul 27 '21 at 23:01
  • I haven't used that before, can you please elaborate? – CompensifyDev Jul 27 '21 at 23:05
  • Firebase is asynchronous - what that means is that it takes time for data to arrive from the Firebase server. Firebase functions use *closures* - that's the section within the brakets {} after the call. Firebase data is valid *within that closure* so if you want to take action once the Firebase data has arrived, that's the place to do it. Keep in mind that FIrebase is incredibly fast - even with large datasets so you probably don't need that progress indicator as it will not be shown long enough to make any difference. (generally speaking) – Jay Jul 28 '21 at 17:31
  • Got it, thanks for the insight! – CompensifyDev Jul 28 '21 at 19:46

2 Answers2

0

There is no progress reporting within a single read operation, either it's pending or it's completed.

If you want more granular reporting, you can implement pagination yourself so that you know how many items you've already read. If you want to show progress against the total, this means you will also need to track the total count yourself though.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
0

You don't need to know the progress of the read as such, just when it starts and when it is complete, so that you can start and stop your activity view.

The read starts when you call getDocuments.

The read is complete after the for loop in the getDocuments completion closure.

So:

let db = Firestore.firestore()

activityIndicatorView.startAnimating()
            
db.collection("Packages").getDocuments{(snapshot, error) in
        
    if error != nil{
            // DB error
    } else {
            
        for doc in snapshot!.documents{
            self.packageIDS.append(doc.documentID)
            self.packageNames.append(doc.get("title") as! String)
            self.packageIMGIDS.append(doc.get("imgID") as! String)
            self.packageRadii.append(doc.get("radius") as! String)
        }
    }
    DispatchQueue.main.async {
        activityIndicatorView.stopAnimating()
    }
}

As a matter of style, having multiple arrays with associate data is a bit of a code smell. Rather you should create a struct with the relevant properties and create a single array of instances of this struct.

You should also avoid force unwrapping.

struct PackageInfo {
     let id: String
     let name: String
     let imageId: String
     let radius: String
}

...


var packages:[PackageInfo] = []

...   
    db.collection("Packages").getDocuments{(snapshot, error) in
        
    if error != nil{
            // DB error
    } else if let documents = snapshot?.documents {

        self.packages = documents.compactMap { doc in
            if let title = doc.get("title") as? String,
               let imageId = doc.get("imgID") as? String,
               let radius = doc.get("radius") as? String {
                   return PackageInfo(id: doc.documentID, name: title, imageId: imageId, radius: radius)
            } else {
               return nil
            }
        }
    }
        
Paulw11
  • 108,386
  • 14
  • 159
  • 186
  • Thank you so much, appreciate the extra tips! Two questions: 1. Since the firebase data functions are asynchronous, how does the DispatchQueue.main.async "bypass" that? 2. How can I access a PackageInfo object/struct using the id that was specified (id: doc.documentID)? {i.e. if I want to modify some fields in the struct for a certain document id [realtime updates]} – CompensifyDev Jul 28 '21 at 04:14
  • The dispatch queue main async doesn't bypass the asynchronous fetch. It just ensures that the update to the UI (stopping the activity indicator) occurs on the main queue as all UI updates must be performed on the main queue. Everything in the closure executes after the fetch is complete – Paulw11 Jul 28 '21 at 04:35
  • Well, you have the document ID so you can either fetch it and apply the updates. The other way you could do it is store the whole document in your struct – Paulw11 Jul 28 '21 at 05:01
  • That makes sense about the dispatch queue, thank you. And about the package struct, right, but how do I fetch the package with that id? Do I need to manually search the struct and note the index where that package id matches, and use that index to, for example, remove that package struct from the array? I know with arrays there is .indexOf() and .remove(at:), does a similar former exist for a struct array where each struct is associated with an id...? – CompensifyDev Jul 28 '21 at 19:51
  • I don't know how you are using the array. If you are showing the data in a tableview then you typically know the index you are working with. Otherwise you can search the array (not very efficient) or use a dictionary of id:package. – Paulw11 Jul 28 '21 at 21:01