I have the following function and it's working great but now I need the value out of the function to use it for another purpose.
But my problem is that I always have to execute the function twice to get a value in the outer part of the function (var valueOutOfFunction).
var valueOutOfFunction = [(String)]()
func loadQuery(com:#escaping( [(Int, String)] -> ())){
var arrayOfTuples = [(Int, String)]()
db.collection("Data").whereField("age", isGreaterThanOrEqualTo: 1).whereField("age", isLessThanOrEqualTo: 50).whereField("gender", isEqualTo: "F").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for (index, document) in querySnapshot!.documents.enumerated() {
arrayOfTuples += [(index, document.documentID)]
}
}
com(arrayOfTuples)
}
}
Then I am calling it here:
loadQuery { arr in
self.valueOutOfFunction = arr // Has a value in the first execution
}
print (valueOutOfFunction) //Executing the first time there is no value in the variable, the second time it have a value.
Why is it just on the second attempt available and what could be a solution for this problem ?
Thanks!
That is because the valueOutOfFunction is happening inside of that closure which is being called asynchronously to the print statement. Additionally, you are executing the completion outside of the else statement which may be firing before your for loop completes. What you want to do is control the flow from within the closure itself like so:
// move com(arrayOfTuples) up into the else block but outside of the for loop
func handleQuery() {
loadQuery { doStuffWithResult($0) }
// This line will run before the first line of doStuffWithResult
// It would be best to end the function logic at loadQuery
}
func doStuffWithResult(_ arrayOfTuples: [(Int, String)]) {
print(arrayOfTuples)
// Do other work here
}
What you're looking into is control flow. You want to execute X if Y has occurred and Z if !Y right?
I would recommend looking into swift's result type. This will help you manage control flow with closures.
An example:
typealias QueryResult = [(Int, String)]
enum QueryErrors: Error {
case couldNotFind
}
func loadQuery(_ result: #escaping (Result<QueryResult, Error>) -> Void) {
db.collection("Data").whereField("age", isGreaterThanOrEqualTo: 1).whereField("age", isLessThanOrEqualTo: 50).whereField("gender", isEqualTo: "F").getDocuments() { (querySnapshot, err) in
guard let documents = querySnapshot?.documents.enumerated() else {
// I think enumerated will give you an array of tuples... if not add this to the end of that line. After enumerated but before else
// enumerated().compactMap { ($0, $1) }
result(.failure(err ?? QueryErrors.couldNotFind))// passes back Firebase error if it exists or default error if it doesn't
return
}
result(.success(documents))
}
}
// how it works
loadQuery() { result in
switch(result) {
case .success(let arr): print(arr)
case .failure(let error): print(error)
}
}
Related
Still learning some swift and managed to advance and retrieving data from a firestore database. I have a Data Controller whose task is to offload all the data retrieving from firestore. It does the calls and gets data, but when returning the info from the first function I have implemented on it, it's empty.
Here's an example of the funcion:
func fetchUnidades(for foo: MyFirstEnum, and bar: MySecondEnum ) -> [MyClassType]{
let db = Firestore.firestore()
let colPath = "my/firebase/path"
let results = [MyClassType]()
let collection = db.collection(colPath)
collection.whereField("myField", isEqualTo: foo.rawValue).getDocuments() { querySnapshot, err in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
do {
print("\(document.documentID) => \(document.data())") // 1st print
let newMyClass = try document.data(as: MyClassType.self)
results.append(newMyClass)
print("here") // 2nd print - debug breakpoint
}catch (let error) {
print("\(error)")
}
}
}
}
print("DC - Recovered \(results.count) results")
return results
}
Assume MyFirstEnum, MySecondEnum and MyClassType are correct, because the database retrieves info. On the 1st print line, there's output for data retrieved, and on the 2nd print - debug breakpoint line, if I do a po results, it has one value, which is the one retrieved as you can see here:
unidades being the name on my code of results on this example.
But right after continuing with the execution, unidades, aka results is empty, the line:
print("DC - Recovered \(results.count) results")
prints DC - Recovered 0 results and the return also returns an empty array with zero values on it.
Any idea about why this might be happening? And how to solve the issue? Obviously the goal is to return the info...
That's because the result comes asynchronously. Your fetchUnidades returns results array before it's populated.
You need to add a completion closure in this case. Instead of returning results you call that completion closure and pass the results as its argument.
func fetchUnidades(for foo: MyFirstEnum, and bar: MySecondEnum, completion: (results: [MyClassType]?, error: Error?) -> Void) {
let db = Firestore.firestore()
let colPath = "my/firebase/path"
let collection = db.collection(colPath)
collection.whereField("myField", isEqualTo: foo.rawValue).getDocuments() { querySnapshot, err in
if let err = err {
print("Error getting documents: \(err)")
completion(nil, err)
} else {
let results = [MyClassType]()
for document in querySnapshot!.documents {
do {
print("\(document.documentID) => \(document.data())") // 1st print
let newMyClass = try document.data(as: MyClassType.self)
results.append(newMyClass)
print("here") // 2nd print - debug breakpoint
}catch (let error) {
print("\(error)")
}
}
completion(results, nil)
}
}
I am trying to Apollo framework and a graphql api to obtain the data then return it. Once I have the data in another swift file, I want to call on certain parts of the data and assign it to a variable. The errors I get is variable used before it is initialized. and if try to return the variable from within the closure I get "Unexpected Non-Void Return Value In Void Function ". I heard of ways to get around that error but I don't completely understand it and how it works with my code. If you need more code or context you can message me and I can share my GitHub repo. Sorry if the code is bad, please don't roast me. I am still a beginner.
import Foundation
import Apollo
struct AniListAPI {
let aniListUrl = "https://graphql.anilist.co"
func ObtainData(AnimeID: Int)-> QueryQuery.Data{
var theData: QueryQuery.Data
let theInfo = QueryQuery(id: AnimeID)
GraphClient.fetch(query: theInfo) { result in
switch result {
case .failure(let error):
print("A big No no happened \(error)")
case .success(let GraphQLResult):
guard let Info = GraphQLResult.data else {return}
theData = Info
}
}
return theData
}
}
Unexpected Non-Void Return Value In Void Function.
The reason you're getting this warning is because you can't return value from inside the closure. Use closure instead of returning value.
func ObtainData(AnimeID: Int, completion: #escaping (Data) -> Void) {
var TheData: QueryQuery.Data
let TheInfo = QueryQuery(id: AnimeID)
GraphClient.fetch(query: TheInfo) { result in
switch result {
case .failure(let error):
print("A big no no happened retard \(error)")
case .success(let GraphQLResult):
guard let Info = GraphQLResult.data else {return}
TheData = Info
completion(TheData)
}
}
}
and call it like..
ObtainData(AnimeID: 123) { (anyData) in
print (anyData)
// continue your logic
}
I am trying to read the value of a Firestore document. I have tried doing it two different ways, but each fails.
In the first one, an error is thrown on the return line: Unexpected non-void return value in void function. I found out why this happened, and so, I implemented the second way.
import UIKit
import Firestore
func readAvailableLists(forUser user: String) -> [String] {
let db = Firestore.firestore()
db.collection("userslist").document(user).getDocument { (document, err) in
if let document = document, document.exists {
return UserInformationDocument(dictionary: document.data()!)?.lists!
} else {
print("Document does not exist")
}
}
}
In the second method, I assign the UserInformationDocument(dictionary: document.data()!)?.lists! to a variable and return that variable at the end of the function (see code below). However, when I do this, it the function returns an empty array. What surprises me is that the print return the correct value, but after long after the function has executed the return statement. Is it because it is an async demand? And if so, how should I fix this?
import UIKit
import Firestore
func readAvailableLists(forUser user: String) -> [String] {
let db = Firestore.firestore()
var firestoreUserDocument: [String] = []
db.collection("userslist").document(user).getDocument { (document, err) in
if let document = document, document.exists {
firestoreUserDocument = (UserInformationDocument(dictionary: document.data()!)?.lists!)!
print((UserInformationDocument(dictionary: document.data()!)?.lists!)!)
} else {
print("Document does not exist")
}
}
return firestoreUserDocument
}
The Firebase call is an asynchronous function. It takes extra time to execute because it's talking to a server (as you've noted) - as a result, the completion block (the block that defines document and err in your example) happens at a different time, outside of the rest of the body of the function. This means you can't return a value from inside it, but you can pass another closure through to it, to execute later. This is called a completion block.
func readAvailableLists(forUser user: String, completion: #escaping ([String]?, Error?) -> Void) -> [String] {
let db = Firestore.firestore()
db.collection("userslist").document(user).getDocument { (document, err) in
if let document = document, document.exists {
// We got a document from Firebase. It'd be better to
// handle the initialization gracefully and report an Error
// instead of force unwrapping with !
let strings = (UserInformationDocument(dictionary: document.data()!)?.lists!)!
completion(strings, nil)
} else if let error = error {
// Firebase error ie no internet
completion(nil, error)
}
else {
// No error but no document found either
completion(nil, nil)
}
}
}
You could then call this function elsewhere in your code as so:
readAvailableLists(forUser: "MyUser", completion: { strings, error in
if let strings = strings {
// do stuff with your strings
}
else if let error = error {
// you got an error
}
})
So my goal here is to basically perform a query in a while loop and append results of the query to my array. When I run the code my "level" variable starts from zero and increments infinitely. I'm highly convinced that my problem is caused by fact that my code is running on 2 async queues but just can't figure out the exact cause.
func displayPathOf(argument: Argument, threadTableView: UITableView) {
array.removeAll()
threadTableView.reloadData()
var level = argument.level!-1
array.insert(argument, at: 0)
var stop = false
DispatchQueue.global(qos: .userInteractive).async {
repeat {
level += 1
print(level)
let query = Argument.query()?.whereKey("level", equalTo: level).addDescendingOrder("reach")
query?.getFirstObjectInBackground(block: { (object, error) in
if object != nil {
DispatchQueue.main.async {
array.append(object as! Argument)
print(array)
threadTableView.reloadData()}
} else {
stop = true
print(error)
}
})
} while stop == false
}
}
Your code boils down to:
do-in-background {
repeat {
level += 1
do-in-background { ... }
} while stop == false
}
do-in-background (i.e. both async and getFirstObjectInBackground) returns immediately, so from the point of view of this loop, it doesn't matter what's in the block. This is equivalent to a tight loop incrementing level as fast as it can.
It looks like you're trying to serialize your calls to getFirstObjectInBackground. You can do that one of two ways:
Have your completion block kick off the next search itself and remove the repeat loop.
Use a DispatchGroup to wait until the completion block is done.
In your case, I'd probably recommend the first. Get rid of stop and make a function something (vaguely) like:
func fetchObject(at level: Int) {
let query = Argument.query()?.whereKey("level", equalTo: level).addDescendingOrder("reach")
query?.getFirstObjectInBackground(block: { (object, error) in
if let object = object {
DispatchQueue.main.async {
array.append(object as! Argument)
print(array)
threadTableView.reloadData()}
// Schedule the next loop
DispatchQueue.global(qos: .userInteractive).async { fetchObject(level + 1) }
} else {
print(error)
}
})
}
I have the following function with throws:
func doSomething(_ myArray: [Int]) throws -> Int {
if myArray.count == 0 {
throw arrayErrors.empty
}
return myArray[0]
}
But when the array is equal to 0 it will throw the error but it will continue executing the function returning empty array.
How can I exit the function when goes to the if statement ?
The function does return when you throw an error. Pop this in a playground and look at the output.
//: Playground - noun: a place where people can play
enum MyError: Error {
case emptyArray
}
func doSomething<T>(_ array: [T]) throws -> T {
guard !array.isEmpty else {
throw MyError.emptyArray
}
return array[0]
}
do {
let emptyArray: [Int] = []
let x = try doSomething(emptyArray)
print("x from emptyArray is \(x)")
} catch {
print("error calling doSeomthing on emptyArray: \(error)")
}
do {
let populatedArray = [1]
let x = try doSomething(populatedArray)
print("x from populatedArray is \(x)")
} catch {
print("error calling doSeomthing on emptyArray: \(error)")
}
You'll see the output is
error calling doSeomthing on emptyArray: emptyArray
x from populatedArray is 1
Note that you don't see the output from print("x from emptyArray is \(x)") because it was never called, because throw ends the execution of that function. You can also confirm this because guard statements require the function to exit.
Also, just to note, if you want the first thing from an array you can just use myArray.first which will return T?, and you can handle the nil case instead of having to deal with an error. For example:
//: Playground - noun: a place where people can play
let myArray = [1, 2, 3]
if let firstItem = myArray.first {
print("the first item in myArray is \(firstItem)")
} else {
print("myArray is empty")
}
let myEmptyArray: [Int] = []
if let firstItem = myEmptyArray.first {
print("the first item in myEmptyArray is \(firstItem)")
} else {
print("myEmptyArray is empty")
}
which outputs:
the first item in myArray is 1
myEmptyArray is empty
You need to understand error handling. If you insert a throws keyword, anybody calling it will need to use a do-catch statement, try?, try! or continue to propagate them. So what will happen if the error is thrown is up to the caller.
Here's an example:
do {
try doSomething(foo)
} catch arrayErrors.empty {
fatalError("Array is empty!")
}
Apple's documentation for error handling
If you want to exit once you reach the if, simply don't use error handling and call fatalError inside the if.
if myArray.count = 0 {
fatalError("Error happened")
}