salman
salman

Reputation: 27

Completion Handler not working Properly in swift

im trying to populate two arrays with the data i get from the firestore database. im getting the data successfully however it was late and when i printed them in viewDidLoad it printed empty arrays. so i decided to implement a completion handler however it still shows and empty array. can anyone tell me why my print statement runs before the functions even though im using escaping

func yourFunctionName(finished: @escaping () -> Void) {
    db.collection("countries")
        .whereField("capital", isEqualTo: "washington")
        .getDocuments { (snapshot, error) in
        if error == nil{
            for document in snapshot!.documents {
                let documentData = document.data()

                //print(document.documentID)
                //print(documentData)

                self.countries.append(document.documentID)
            }

        }
    }

    db.collection("countries")
        .whereField("climate", isEqualTo: "pleasant")
        .getDocuments { (snapshot, error) in
        if error == nil {
            for document in snapshot!.documents{
                let documentData = document.data()

                //print(document.documentID)
                //print(documentData)

                self.countries2.append(document.documentID)
            }
        }
    }
    finished()

}

viewDidLoad(){
    yourFunctionName {
        print(self.countries)
        print(self.countries2)
    }
}

i get the empty arrays in the output although the arrays should have been filled before i called print though im using @escaping. please someone help me here

Upvotes: 1

Views: 2783

Answers (4)

ViTUu
ViTUu

Reputation: 1204

I think your main problem here is not about to populate your arrays, your problem is how to get it better.

I did an example of how you could do that in a better way.

First, break your big function in two, and populate it out of your function.

Look at this code and observe the viewDidLoad implementation.

func countries(withCapital capital: String, completionHandler: (Result<Int, Error>) -> Void) {
        db.collection("countries")
        .whereField("capital", isEqualTo: capital)
        .getDocuments { (snapshot, error) in
            guard error == nil else {
                completionHandler(.failure(error!))
                return

            }

            let documents = snapshot!.documents
            let ids = documents.map { $0.documentID }
            completionHandler(.success(ids))
    }

}

func countries(withClimate climate: String, completionHandler: (Result<Int, Error>) -> Void) {
        db.collection("countries")
        .whereField("climate", isEqualTo: climate)
        .getDocuments { (snapshot, error) in
            guard error == nil else {
                completionHandler(.failure(error!))
                return

            }

            let documents = snapshot!.documents
            let ids = documents.map { $0.documentID }
            completionHandler(.success(ids))
    }
}


func viewDidLoad(){
    countries(withClimate: "pleasant") { (result) in
        switch result {
        case .success(let countries):
            print(countries)
            self.countries2 = countries
        default:
            break
        }
    }

    countries(withCapital: "washington") { (result) in
        switch result {
        case .success(let countries):
            print(countries)
            self.countries = countries
        default:
            break
        }
    }
}

If you have to call on main thread call using it

DispathQueue.main.async {
   // code here
}

I hope it helped you.

Upvotes: 1

La pieuvre
La pieuvre

Reputation: 468

You are actually not escaping the closure. For what I know the "@escaping" is a tag that the developper of a function use to signify the person using the function that the closure he/she is passing will be stored and call later (after the function ends) for asynchronicity and memory management. In your case you call the closure passed immediately in the function itself. Hence the closure is not escaping.

Also the firebase database is asynchronous. Meaning that you don't receive the result immediately

This part :

{ (snapshot, error) in
    if error == nil{
        for document in snapshot!.documents {
            let documentData = document.data()

            //print(document.documentID)
            //print(documentData)

            self.countries.append(document.documentID)
        }

    }
}

is itself a closure, that will be executed later when the result of the query is produced. As you can see in the doc, the function is escaping the closure : https://firebase.google.com/docs/reference/swift/firebasefirestore/api/reference/Classes/Query.html#getdocumentssource:completion:

func getDocuments(source: FirestoreSource, completion: @escaping FIRQuerySnapshotBlock)

So to summarise : The code for the firebase query will be call later (but you don't know when), and your closure "finished" is called immediately after having define the firebase callback, thus before it has been called.

You should call your finished closure inside the firebase callback to have it when the arrays are populated.

Upvotes: 1

Edwin Lileo
Edwin Lileo

Reputation: 54

I has been sometime since I encountered that problem but I beleve the issue is that you are calling the completion handler to late. What I mean is that you can try to call it directly after you have lopped throught the documents. One idea could be to return it in the compltion or just do as you do. Try this instead:

func yourFunctionName(finished: @escaping ([YourDataType]?) -> Void) {
        var countires: [Your Data Type] = []
        db.collection("countries")
            .whereField("capital", isEqualTo: "washington")
            .getDocuments { (snapshot, error) in
            if error == nil{
                for document in snapshot!.documents {
                    let documentData = document.data()
                    //print(document.documentID)
                    //print(documentData)
                    countries.append(document.documentID)
                }
               finished(countries)
               return
            }
        }
    }

    func yourSecondName(finished: @escaping([YouDataType]?) -> Void) {
    var countries: [Your data type] = []

    db.collection("countries")
            .whereField("climate", isEqualTo: "pleasant")
            .getDocuments { (snapshot, error) in
            if error == nil {

                for document in snapshot!.documents{
                    let documentData = document.data()
                    //print(document.documentID)
                    //print(documentData)
                   countires.append(document.documentID)
                }
              finished(countires)
              return
            }
        }

    func load() {
        yourFunctionName() { countries in
           print(countires)
        }
        yourSecondName() { countries in
           print(countries)
        }
   }

   viewDidLoad(){
        load()
    }

What this will do is that when you call the completion block that is of type @escaping as well as returning after it you won't respond to that completely block any longer and therefore will just use the data received and therefore not care about that function anymore.

I good practice according to me, is to return the object in the completion block and use separate functions to be easier to debug and more efficient as well does it let you return using @escaping and after that return.

You can use a separate method as I showed to combine both methods and to update the UI. If you are going to update the UI remember to fetch the main queue using:

DispathQueue.main.async {
    // Update the UI here
}

That should work. Greate question and hope it helps!

Upvotes: 0

Mostafa ElShazly
Mostafa ElShazly

Reputation: 526

They are returning empty arrays because Firebase's function is actually asynchronous (meaning it can run after the function "yourFunctionName" has done its work) in order for it to work as intended (print the filled arrays) All you need to do is call it inside Firebase's closure itself, like so:

func yourFunctionName(finished: @escaping () -> Void) {
db.collection("countries")
    .whereField("capital", isEqualTo: "washington")
    .getDocuments { (snapshot, error) in
    if error == nil{
        for document in snapshot!.documents {
            let documentData = document.data()
            self.countries.append(document.documentID)
            finished() //<<< here
        }

    }
}

db.collection("countries")
    .whereField("climate", isEqualTo: "pleasant")
    .getDocuments { (snapshot, error) in
    if error == nil {
        for document in snapshot!.documents{
            let documentData = document.data()
            self.countries2.append(document.documentID)
            finished() //<<< and here
        }
    }
}

}

Upvotes: 0

Related Questions