Can't assign variable to Firebase Data within getDocuments querysnapshot (Swift) [duplicate] - swift

This question already has answers here:
Storing asynchronous Cloud Firestore query results in Swift
(1 answer)
How do I save data from cloud firestore to a variable in swift?
(1 answer)
Closed 1 year ago.
I'm trying to set a variable to a boolean based on this code here:
Firestore.firestore().collection("users").whereField("username", isEqualTo: usernameTextField.text!).getDocuments
{ (querySnapshot, error) in
if let error = error { print(error.localizedDescription) /*ALERT*/ }
else
{
if querySnapshot!.isEmpty
{
print("QuerySnapshot is empty, this is unique")
retVal = true
}
else
{
print("There's a bloody username in here, get a new one")
retVal = false
}
}
}
return retVal
The issue is however, retVal is not changed. I understand what's going on here, it's an asynchronous block of code, however I don't understand how to fix it or work around it to fit my needs. How do I get the value of retVal, (or honestly just return true or false within this block of code) despite it being asynchronous. Is there anyway I can like wait for it to finish before further execution of code?

Since your call is asynchronous, you should use a callback or completion handler in your function, this way the retVal will be returned when the Firestore query finishes.
Something like this:
func userIsUnique(completionHandler: (Bool) -> Void) {
Firestore.firestore().collection("users").whereField("username", isEqualTo: usernameTextField.text!).getDocuments
{ (querySnapshot, error) in
if let error = error { print(error.localizedDescription) /*ALERT*/ }
else
{
if querySnapshot!.isEmpty
{
print("QuerySnapshot is empty, this is unique")
completionHandler(true)
}
else
{
print("There's a bloody username in here, get a new one")
completionHandler(false)
}
}
}
}

Related

Swift problem with Firestore data retrieval [duplicate]

This question already has answers here:
Retrieve data from array in another function
(1 answer)
Is Firebase getDocument run line by line?
(1 answer)
Storing asynchronous Cloud Firestore query results in Swift
(1 answer)
SwiftUI - Wait until Firestore getDocuments() is finished before moving on
(1 answer)
Closed last year.
I have been searching a while and I could not find a solution -> me opening a new thread.
I am making this register page which works with a token send to the users email, it has to be checked against the values in the database, however (in code below) it prints "false" before "hi", I have been trying to get it async, but it doesn't want to work. Can someone help me out? Thanks!
PS: The selection variable makes my app go to the next page in registration, which, because return is false, won't go to.
public static func tokenVerify(_ token: String) -> Bool{
// Get database
let db = Firestore.firestore()
let collection = db.collection("tokens")
let query = collection.whereField("token", isEqualTo: token)
// Make response
var response = false
// Get all documents
query.getDocuments { snapshot, error in
// Check for errors,
if error == nil{
// No error
if let snapshot = snapshot {
if snapshot.isEmpty {
print("No token in database")
} else {
response = true
print("hi")
}
} else {
print("Unknown Error")
}
} else {
print("Error retrieving documents.")
}
}
print(response)
return response
}
Main View:
DispatchQueue.main.async{
if Registration.tokenVerify(token) {
selection = "third"
}
}

Why am I still able to fetch data, even with deleting FireStore object in Swift?

I deleted an entry in the Firestore and also checked it manually to confirm that. However, as long as I do not close the application, I can send a request to fetch the data and I still get the result. This should not be the case.
If you imagine having a shared photo with some textual information and you delete those information, this would mean, other users can still see the textual information (fetched from the Firestore) but not the image anymore (store in Firestorage).
I want to display a message on the UI, something like "The content does not exist anymore".
How I can achieve that? I used the following approach so far but it does not work at the moment:
public func checkIfChallengeObjectExists(completionHandler:#escaping(Bool)->(), challengeId:String) {
CHALLENGE_COLLECTION?.document(challengeId).getDocument(completion: { (querySnapshot, error) in
if (error != nil) {
print(error?.localizedDescription as Any)
}
if (querySnapshot?.documentID == "" || querySnapshot!.metadata.isFromCache) {
completionHandler(false)
}
else {
completionHandler(true)
}
})
}
Any solutions?
Non-existent documents will still return document snapshots, but they will be empty. Therefore, you must check the contents of the snapshot for the document, not the snapshot itself. Also, you should handle errors and the overall flow of the return better.
public func checkIfChallengeObjectExists(completionHandler:#escaping(Bool)->(), challengeId:String) {
CHALLENGE_COLLECTION?.document(challengeId).getDocument(completion: { (querySnapshot, error) in
if let doc = querySnapshot,
doc.exists {
completionHandler(true) // only one possible true condition
} else {
if let error = error {
print(error.localizedDescription)
}
completionHandler(false) // all else false
}
})
}
As a side note, I recommend reordering the parameters of the function to make it easier to read when called (conventionally, the completion handler comes last) and giving the boolean argument a name so it's easier to read when referencing (sometime later or by other developers).
public func verifyChallengeObject(ID: String, _ completion: #escaping (_ exists: Bool) -> Void) {
...
}
verifyChallengeObject(ID: "abc123", { (exists) in
if exists {
...
} else {
...
}
})

Can't assign a value to variable inside of closure Swift [duplicate]

This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 2 years ago.
I'm trying to get current document count belongs to spesific user from Firebase.
Whenever i try to assign count value to my variable in closure, value is always shown as nill.
So that i did couple of research, as a result i figured out networking sometimes takes so long and it happens asynchronously. So if i'm not wrong due to asynchronous returning a value inside a fucntion might happen before assign value .
I tried to add dispatchqueue.main.async but it didn't work for me...
Here is my code
func getEventCount () -> Int? {
var count: Int?
db.collection("Events").whereField("owner", isEqualTo: currentUser.email).getDocuments { (snapshot, error) in
if error != nil {
print(error)
}else {
DispatchQueue.main.async {
if let snapshot = snapshot {
count = snapshot.count
}
}
}
}
return count
}
My main goal is getting count data from database and assign it to 'count' variable.
So why do i need that count variable? Because i'm going to pass that count value to tableView datasource method numberOfRowsInSection which is expecting a int value. With that value i'm going to represent some data in Events document from firestore in table views.
note: When i try to print count value in closure it shows desired value, but when function return value it's shown nill...
Once it is a Async call - you cannot synchronously return the value from the function. You should accept a callback to the function that will accept the count. That callback function or closure will be passed the value asynchronously.
func getEventCount (callback: #escaping(Result<Int, Error>) -> Void) {
db.collection("Events").whereField("owner", isEqualTo: currentUser.email).getDocuments { (snapshot, error) in
if error != nil {
let result = Result.failure(error)
callback(result)
}else if let snapshot = snapshot {
let result = Result.success(snapshot.count)
callback(result)
} else {
let result = Result.failure(SomeCustomAppError)
callback(result)
}
}
}
Then you can call this function passing in a callback
self.getCount() { result in
switch result {
case .success(let count): /// use count
print(count)
// only here u can assign the count value to ur variable
case .error(let error): /// handle error
print(error.localizedDescription)
}
}
Note: In the above I've used the Result datatype from Swift standard library - https://developer.apple.com/documentation/swift/result so that both error
or result can be passed back

Assign value of a Firestore document to a variable

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
}
})

Convert recursive async function to promise

I have a recursive, async function that queries Google Drive for a file ID using the REST api and a completion handler:
func queryForFileId(query: GTLRDriveQuery_FilesList,
handler: #escaping FileIdCompletionHandler) {
service.executeQuery(query) { ticket, data, error in
if let error = error {
handler(nil, error)
} else {
let list = data as! GTLRDrive_FileList
if let pageToken = list.nextPageToken {
query.pageToken = pageToken
self.queryForFileId(query: query, handler: handler)
} else if let id = list.files?.first?.identifier {
handler(id, nil)
} else {
handler(nil, nil) // no file found
}
}
}
}
Here, query is set up to return the nextPageToken and files(id) fields, service is an instance of GTLRDriveService, and FileIdCompletionHandler is just a typealias:
typealias FileIdCompletionHandler = (String?, Error?) -> Void
I've read how to convert async functions into promises (as in this thread) but I don't see how that can be applied to a recursive, async function. I guess I can just wrap the entire method as a Promise:
private func fileIdPromise(query: GTLRDriveQuery_FilesList) -> Promise<String?> {
return Promise { fulfill, reject in
queryForFileId(query: query) { id, error in
if let error = error {
reject(error)
} else {
fulfill(id)
}
}
}
}
However, I was hoping to something a little more direct:
private func queryForFileId2(query: GTLRDriveQuery_FilesList) -> Promise<String?> {
return Promise { fulfill, reject in
service.executeQuery(query) { ticket, data, error in
if let error = error {
reject(error)
} else {
let list = data as! GTLRDrive_FileList
if let pageToken = list.nextPageToken {
query.pageToken = pageToken
// WHAT DO I DO HERE?
} else if let id = list.files?.first?.identifier {
fulfill(id)
} else {
fulfill(nil) // no file found
}
}
}
}
}
So: what would I do when I need to make another async call to executeQuery?
If you want to satisfy a recursive set of promises, at where your "WHAT DO I DO HERE?" line, you'd create a new promise.then {...}.else {...} pattern, calling fulfill in the then clause and reject in the else clause. Obviously, if no recursive call was needed, though, you'd just fulfill directly.
I don't know the Google API and you didn't share your code for satisfying a promise for a list of files, so I'll have to keep this answer a bit generic: Let's assume you had some retrieveTokens routine that returned a promise that is satisfied only when all of the promises for the all files was done. Let's imagine that the top level call was something like:
retrieveTokens(for: files).then { tokens in
print(tokens)
}.catch { error in
print(error)
}
You'd then have a retrieveTokens that returns a promise that is satisfied only when then promises for the individual files were satisfied. If you were dealing with a simple array of File objects, you might do something like:
func retrieveTokens(for files: [File]) -> Promise<[Any]> {
var fileGenerator = files.makeIterator()
let generator = AnyIterator<Promise<Any>> {
guard let file = fileGenerator.next() else { return nil }
return self.retrieveToken(for: file)
}
return when(fulfilled: generator, concurrently: 1)
}
(I know this isn't what yours looks like, but I need this framework to show my answer to your question below. But it’s useful to encapsulate this “return all promises at a given level” in a single function, as it allows you to keep the recursive code somewhat elegant, without repeating code.)
Then the routine that returns a promise for an individual file would see if a recursive set of promises needed to be returned, and put its fulfill inside the then clause of that new recursively created promise:
func retrieveToken(for file: File) -> Promise<Any> {
return Promise<Any> { fulfill, reject in
service.determineToken(for: file) { token, error in
// if any error, reject
guard let token = token, error == nil else {
reject(error ?? FileError.someError)
return
}
// if I don't have to make recursive call, `fulfill` immediately.
// in my example, I'm going to see if there are subfiles, and if not, `fulfill` immediately.
guard let subfiles = file.subfiles else {
fulfill(token)
return
}
// if I got here, there are subfiles and I'm going to start recursive set of promises
self.retrieveTokens(for: subfiles).then { tokens in
fulfill(tokens)
}.catch { error in
reject(error)
}
}
}
}
Again, I know that the above isn't a direct answer to your question (as I'm not familiar with Google Drive API nor how you did your top level promise logic). So, in my example, I created model objects sufficient for the purposes of the demonstration.
But hopefully it's enough to illustrate the idea behind a recursive set of promises.