1

Thanks in advance for the help. I'm teaching myself Swift and trying to figure out how to retrieve the following data from Firebase. Here's my Firebase Data Model...

Groups (Collection) -> GroupName (String) -> Owner (References to someone in the Players collection)

Players (Collection) -> PlayerFirstName -> PlayerLastName

The Swift I've written to retrieve this data is in a ViewModel. getAllGroups is called from onAppear in the View and looks like this...

class Group: Identifiable, ObservableObject {
    var id: String = UUID().uuidString
    var name: String?
    var owner: Player?
}

class GroupViewModel: ObservableObject {

    @Published var groups = [Group]()
    private var db = Firestore.firestore()

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

            self.groups = documents.map { (queryDocumentSnapshot) -> Group in
                var group = Group()
                let data = queryDocumentSnapshot.data()
                group.name = data["name"] as? String ?? ""

//
// LIKE --- SHOULD THIS CALL TO GETPLAYER use AWAIT, FOR EXAMPLE?
// WE'RE EXECUTING THE CLOSURE FOR THE FIRST CALL AND ABOUT TO MAKE A SECOND
//
                group.owner = self.getPlayer(playerRef: data["owner"] as! DocumentReference)
                return group
            }
        }
    }

    func getPlayer(playerRef: DocumentReference) -> Player {

        var player = Player()
        
        playerRef.getDocument { (document, error) in
            guard error == nil else {
                print ("error", error ?? "")
                return
            }
            
            if let document = document, document.exists {
                let data = document.data()
                if let data = data {
                    player.firstName = data["firstname"] as? String
                    player.lastName = data["lastname"] as? String
                }
            }
        }

        return player
    }
}

The sorta obvious problem here is the closure for retrieving the parent Group executes and then goes and tries to retrieve the Owner. But by the time the closure inside getPlayer completes... the Group has already been established.

Groups will have...

group[0]
-> GroupName = "Cool Name Here"
-> Owner = nil

group[0]
-> GroupName = "Different Cool Name"
-> Owner = nil

even though each Group definitely has an Owner.

I get there's some stuff here about asynchronous calls in Swift and how best to handle that... I'm just not sure what the proper pattern is. Thanks again for the help and advice!

-j

Jason
  • 11
  • 2
  • getPlayer(playerRef: DocumentReference) will allways return an empty Player because you do not wait for the network call to execute. You should dive more into async and/or closures in swift. – burnsi Mar 02 '22 at 09:35
  • thanks burnsi. makes sense. i think you're saying, for example, that moving 'return player' inside the closure should work? – Jason Mar 02 '22 at 18:59
  • No. This cannot be explained in a comment or an answer. You are missing the basics of async programming. Try the firebase documentation for a way to implement this. – burnsi Mar 02 '22 at 19:38
  • I think these questions and answers may help [this](https://stackoverflow.com/questions/62199268/swift-firebase-storage-code-block-not-executing/62201584#62201584) and [this](https://stackoverflow.com/questions/43027817/how-to-perform-an-action-only-after-data-are-downloaded-from-firebase/43029121#43029121). And [this one](https://stackoverflow.com/questions/56442492/searchbar-problem-while-trying-to-search-firestore-and-reload-the-tableview/56446914#56446914) as it's Firestore specific. Note: you cannot `return` from an asych function. A `completion handler` is one option. – Jay Mar 03 '22 at 20:35
  • thanks jay and burnsi. i think i stated my original question poorly and probably should've written pseudocode to better explain. my snippet isn't very well written. i get async and closures. What i'm asking is... what's the proper pattern for *nesting* these. I'm doing an async call to get the parent document but, on completion, then need another async call to get children references. But by the time the 2nd closure for the children completes... the first is long finished – Jason Mar 04 '22 at 00:54

1 Answers1

0

To restate the question:

How do you nest Firestore functions

There are 100 ways to do it and, a lot of it depends on the use case. Some people like DispatchGroups, others like escaping completion handlers but in a nutshell, they pretty much do the "same thing" as the following code, written out "long hand" for readability

func populateGroupArray() {
    db.collection("groups").addSnapshotListener { (querySnapshot, error) in
        guard let docs = querySnapshot?.documents else { return }

        for doc in docs {
            let groupName = doc.get("name") as! String
            let ownerId = doc.get("owner_id") as! String
            self.addToArray(groupName: groupName, andOwnerId: ownerId)
        }
    }
}

func addToArray(groupName: String, andOwnerId: String) {
    db.collection("owners").document(andOwnerId).getDocument(completion: { snapshot, error in
        let name = snapshot?.get("owner_name") as! String
        let group = Group(groupName: groupName, ownerName: name)
        self.groups.append(group)
    })
}

In summary; calling populateGroupArray reads in all of the documents from the groups collection from Firestore (adding a listener too). We then iterate over the returned documents to get each group name and the owner id of the group.

Within that iteration, the group name and ownerId are passed to another function that reads in that specific owner via it's ownerId and retrieves the name

Finally, a Group object is instantiated with groupName and owner name being populated. That group is then added to a class var groups array.

Now, if you ask a Firebaser about this method, they will generally recommend not reading large amounts of Firebase data 'in a tight loop'. That being said, this will work very well for many use cases.

In the case you've got a HUGE dataset, you may want to consider denormalizing your data by including the owner name in the group. But again, that would be a rare situation.

Jay
  • 34,438
  • 18
  • 52
  • 81