0

I capture the data from Firestore and assign to array, someone in other question told me maybe I need a completion handler but I don't understand how to work, it would be very helpful if you help me with the code and explain this concept to me.

class PetsTVC: UITableViewController {

    var db: Firestore!
    let uid = Auth.auth().currentUser?.uid
    var petslist = [String]()
    var pid = ""
    var pets = [paw]()

    override func viewDidLoad() {
        super.viewDidLoad()

        self.loadPets()
        DispatchQueue.main.async {
            self.tableView.reloadData()
        }
    }

    func loadPets(){
        let photo1 = UIImage(named: "Log")
        db = Firestore.firestore()

        db.collection("users").document(uid!).getDocument { documentSnapshot, error in
            guard let document = documentSnapshot else {
                return
            }
            self.petslist = document["petslist"] as? Array ?? [""]
        }
        for pet in self.petslist {
            self.db.collection("pets").document(pet).getDocument { documentSnapshot, error in
                guard let document = documentSnapshot else {
                    return
                }
                guard let data = document.data() else {
                    return
                }
                let pid = data["pid"] as! String? ?? ""
                let petName = data["petName"] as! String? ?? ""
                let infoPet = paw(id: pid, petName: petName, imagenMascota:photo1)
                self.pets.insert(infoPet, at: 0)
            }
        }
    }
}

If you need anything else from the code just tell me, because I'm also new here and I don't know what to put in the questions.

  • Does this answer your question? [I try too show data what I have in Firestore in Swift](https://stackoverflow.com/questions/59149130/i-try-too-show-data-what-i-have-in-firestore-in-swift) – koen Dec 03 '19 at 14:48
  • No, for this answer I asked this question. – Juan Camilo Dec 03 '19 at 15:29
  • That code isn't going to work, and you do NOT *need* a completion handler. That's just one of many options. Firestore is *asynchronous* and the data returned within the firebase closure is only valid within that closure, and will take time to return from the internet. So in your case the `for pet` loop will execute WAY before that data is returned. The easy fix is to move the for loop to right after `self.petslist = ` so the loop can work on the data within the closure, where it's valid. – Jay Dec 03 '19 at 18:33

3 Answers3

2

A completion handler is a bit of code you hand to some function that is going to take a moment to do its job and you don't want to have to wait for it to finish before moving on.

Like if your mailbox was a ways away and you sent a kid off to get the mail. It is going to take them a minute to go to the mailbox, get the mail, and come back so you are going to carry on with other things. Since kids forget easily you write some instructions on a piece of paper for what to do with the mail once they bring it back. That piece of paper is the completion handler. Most likely one of the instructions on that piece of paper is to hand some or all of the mail to you but maybe it includes filtering out all the junk mail first or something.

In the code you have shared you actually have two completion handlers; on the line that starts db.collection("users").document(uid!).getDocument and the one that starts self.db.collection("pets").document(pet).getDocument everything that is inside the { } is the completion handler (or closure).

In order to get results into your table you likely need to make two changes. The first change is to move the } from after the line self.petslist = document["petslist"] as? Array ?? [""] so that is after the line self.pets.insert(infoPet, at: 0).

The second change is to move:

DispatchQueue.main.async {
    self.tableView.reloadData()
}

Out of viewDidLoad() and into loadPets() placing it after the line self.pets.insert(infoPet, at: 0). That way the table view reloads after all the data has come in and been processed.

Right now you are telling your kid to go get the mail and put it in a basket but trying to get the mail out of the basket when the kid has barely made it out the front door, you need to wait until the kid gets back, then grab the mail out of the basket.

theMikeSwan
  • 4,739
  • 2
  • 31
  • 44
  • Thanks @theMikeSwan, It makes sense and it worked perfectly and I understood the completion handler a little better – Juan Camilo Dec 03 '19 at 15:36
  • @Camilo, glad it worked. Feel free to tick the checkmark next to the answer that worked for you. – theMikeSwan Dec 03 '19 at 16:00
  • To clarify, UI calls within Firebase closures are performed on the main thread - so `DispatchQueue.main.async` is extraneous. Also note that if there were a LOT of pets, calling `self.tableView.reloadData()` after each pet is loaded will cause flicker. For small datasets it's fine. – Jay Dec 03 '19 at 19:11
1

You don't need a completion handler, you need DispatchGroup to reload the table view after the last fetch.

And you have to put the loop into the completion handler of the first database access

override func viewDidLoad() {
    super.viewDidLoad()
    self.loadPets()
}

func loadPets(){
    let group = DispatchGroup()
    let photo1 = UIImage(named: "Log")
    db = Firestore.firestore()

    db.collection("users").document(uid!).getDocument { documentSnapshot, error in
        guard let document = documentSnapshot else { return }

        self.petslist = document["petslist"] as? [String] ?? []
        if self.petslist.isEmpty {
            DispatchQueue.main.async {
               self.tableView.reloadData()
            }
            return
        }

        for pet in self.petslist {
            group.enter()
            self.db.collection("pets").document(pet).getDocument { documentSnapshot, error in
                defer { group.leave() }

                guard let document = documentSnapshot,
                   let data = document.data() else { return }

                let pid = data["pid"] as? String ?? ""
                let petName = data["petName"] as? String ?? ""
                let infoPet = paw(id: pid, petName: petName, imagenMascota:photo1)
                self.pets.insert(infoPet, at: 0)
           }
        }
        group.notify(queue: .main)  {
           self.tableView.reloadData()
        }
    }
}
vadian
  • 274,689
  • 30
  • 353
  • 361
1

Let's keep this super simple - leveraging completion handlers and dispatch groupds for this task may not be needed.

Here's the problem: Firestore is asynchronous and you can only work with the returned data within the closure following the call. Code after that loop will actually execute before the code in the loop because code is faster than the internet. So for example.

db.collection("users").document(uid!).getDocument... {
    //data is valid here
    print("Test")
}
print("Hello") //this code will execute and print Hello before Test is printed

So here's a straightforward solution to load a users data and then fetch his pets name. We have two collection

users
   uid_0
      name: "Leroy"
      petslist:
         0: "pet_0"
         1: "pet_1"
         2: "pet_2"
pets
   pet_0
      petName: "Spot"
   pet_1
      petName: "Rover"
   pet_2
      petName: "Fluffy"

and the code

func loadPets() {
    let uid = "uid_1"
    self.db.collection("users2").document(uid).getDocument { documentSnapshot, error in
        guard let document = documentSnapshot else {
            return
        }
        let name = document["name"] as! String
        print("Owner \(name) has the following pets:")
        let petList = document["petslist"] as? Array ?? [""]
        for pet in petList {
            self.db.collection("pets").document(pet).getDocument { documentSnapshot, error in
                guard let document = documentSnapshot else {
                    return
                }
                let petName = document.get("petName") as? String ?? "No Pet Name"
                print(petName) //Or reload the tableview**
            }
        }
    }
}

and the output

Owner Leroy has the following pets:
Spot
Rover
Fluffy

That being said, you can incorporate closures as an option to do the same thing but I think that goes beyond the scope of the question.

**for this example, I'm keeping it easy. In reality if you've got a lot of data, refreshing the tableView after each pet is loaded will cause flicker and not a good UI experience. This is where a DispatchGroup can come into play as, for example, you can load all of the pets and when when the last pet is loaded leave the dispatch group and then update the tableView.

@Vadian has a version of DispatchGroups outlined in his answer but here's a variation using a DispatchGroup for loading the pet names:

func loadPets() {
    let uid = "uid_1"

    self.db.collection("users2").document(uid).getDocument { documentSnapshot, error in
        guard let document = documentSnapshot else { return }
        let name = document["name"] as! String
        print("Owner \(name) has the following pets:")

        let group = DispatchGroup()

        let petList = document["petslist"] as? Array ?? [""]

        for pet in petList {
            group.enter()
            self.db.collection("pets").document(pet).getDocument { documentSnapshot, error in
                guard let document = documentSnapshot else { return }
                let petName = document.get("petName") as? String ?? "No Pet Name"
                print(petName)
                group.leave()
            }
        }

        group.notify(queue: .main) {
            print("all pets loaded, reload tableview")
        }
    }
}
Jay
  • 34,438
  • 18
  • 52
  • 81