Reputation: 27
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
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
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
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
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