Hi I am new in iOS development and I am having hard time to understand the following issue. Basically I am trying to get user's name by passing current user's id to Cloud Firestore. However I am having hard time to understand a bug in the code. I can successfully pass the name of user to name variable, while the function returns default value of name which is "" (empty string). It seems that the block of code inside
if let data = snapshot?.data() {
guard let userName = data["name"] as? String else { return }
name = userName
print("after guard") // this line
}
happens later than
print("name") // this line
return name
Full code:
private func returnCurrentUserName() -> String {
// User is signed in.
var name = ""
if let user = Auth.auth().currentUser {
let db = Firestore.firestore()
db.collection("users").document(user.uid).getDocument { (snapshot, error) in
if error == nil {
if let data = snapshot?.data() {
guard let userName = data["name"] as? String else { return }
name = userName
print("after guard") // this line
}
}
}
print("name") // this line
return name
}else {
return ""
}
}
(Note: the query from Cloud Firestore is successful and I can get users name on the console but "name" is printed after "after guard".)
In addition to the other answer:
If you would like to execute code after your operation is done, you could use a completion block (that's just a closure which gets called upon completion):
private func returnCurrentUserName(completion: #escaping () -> ()) -> String {
// User is signed in.
var name = ""
if let user = Auth.auth().currentUser {
let db = Firestore.firestore()
db.collection("users").document(user.uid).getDocument { (snapshot, error) in
if error == nil {
if let data = snapshot?.data() {
guard let userName = data["name"] as? String else { return }
name = userName
completion()//Here you call the closure
print("after guard") // this line
}
}
}
print("name") // this line
return name
}else {
return ""
}
}
How you would call returnCurrentUserName:
returnCurrentUserName {
print("runs after the operation is done")
}
Simplified example:
func returnCurrentUserName(completion: #escaping () -> ()) -> String {
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
completion() //runs after 4 seconds
}
return "xyz"
}
let test = returnCurrentUserName {
print("runs after the operation is done")
}
print(test)
The reason is your getDocument is an asynchronous operation. It takes a callback, and that callback will be invoked when the operation is done. Because of the asynchronous operation, the program will continue process the next line without waiting for the async operation to be completed. That's why you see your print("name") getting executed before the print("after guard")
Related
Im trying to follow the Documentation to pass the result of a promise to the next promise
https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md#saving-previous-results
I keep getting
Contextual closure type '(_) throws -> _' expects 1 argument, but 2 were used in closure body
Here my Code
class func loginInOrSignup() -> Promise<PFUser> {
let lm = LoginManager()
lm.logOut()
return logInInBackground().then{user in
if user.isNew{
return getUserInfo().map{ ($0, user) }
}
else{
user.fetchIfNeededInBackground()
return Promise.value(user)
}
}.then{(userInfo, user) -> Promise<PFUser> in
let name = userInfo["name"] as! String
if let email = userInfo["email"] as? String {
user.email = email
}
let username = G8FacebookLogin.generateSuggestedUsername(name: name)
user.username = username
return Promise.value(user)
}
}
private class func logInInBackground() -> Promise<PFUser>{
return Promise {seal in
PFFacebookUtils.logInInBackground(withReadPermissions: ["public_profile", "email"]){user, error in
guard let user = user else {
seal.reject(error ?? AuthError.msg(reason: "Some FB Error"))
return
}
seal.fulfill(user)
}
}
}
private class func getUserInfo() -> Promise<Dictionary<String,Any>> {
return Promise {seal in
G8FacebookLogin.getUserInfo { (userDict, error) in
guard let userInfo = userDict else{
PFUser.logOut()
seal.reject(AuthError.loginFailed(reason: "no user Info retrieved"))
return
}
seal.fulfill(userInfo)
}
}
}
It's pretty much the same as the Snippet code in the documentation. I don't understand what is the right way to do it.
This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 3 years ago.
the following is printing "no user with username" but is printing retVal as "false" ( I changed function to a string just for troubleshooting, ideally this should be bool ) I am new to swift and this is absolutely driving me crazy. it is making it to the chunk of code where retVal would get reassigned, but it isn't reassigning it
static func isUserNameUnique(_ username : String) -> String {
var retVal = "false"
let db = Firestore.firestore()
let newQuery = db.collection("users").whereField("userName", isEqualTo: username)
newQuery.getDocuments { (document, error) in
if document!.isEmpty {
retVal = "true"
print("No user with username")
}
}
print("\(retVal)")
return retVal
}
func validateFields() -> String? {
//Check that all fields are filled in
if premierCodeTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" || userNameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" {
return "Please fill in all fields."
}
//Check unique username
let cleanedUserName = userNameTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
Utilities.isUserNameUnique(cleanedUserName) { res in
if !res {
// return "please choose a unique username"
}
}
return nil
}
You are trying to return a value synchronously while using and asynchronous method.
When you do newQuery.getDocuments execution continues without waiting for completion handler to be called. So after that line is executed, the return is executed, and THEN the completion handler gets called.
If you want to get a value from an asynchronous method, you need to create a method that takes a completion handler like the answer Khan gave you.
static func isUserNameUnique(_ username: String, completionHandler: #escaping (Bool) -> Void) {
let db = Firestore.firestore()
let newQuery = db.collection("users").whereField("userName", isEqualTo: username)
newQuery.getDocuments { (document, error) in
completionHandler(document!.isEmpty)
}
}
You need a completion as the request is asynchnous , plus use Bool instead of a String
static func isUserNameUnique(_ username : String,completion:#escaping((Bool) ->())) {
let db = Firestore.firestore()
let newQuery = db.collection("users").whereField("userName", isEqualTo: username)
newQuery.getDocuments { (document, error) in
completion(document!.isEmpty)
}
}
Call
Utilities.isUserNameUnique { res in
if !res {
// name exists
}
}
It's impossible to achieve what you want since newQuery.getDocuments isn't returning value instantly. It will answer you at some point by calling function that you passed to it.
Your code can be described as
func foo() -> String {
// set retVal to "false"
var retVal = "false"
// create query
let db = Firestore.firestore()
let newQuery = db.collection("users").whereField("userName", isEqualTo: username)
// ask query to evaluate
newQuery.getDocuments { (document, error) in
// at some point probably after foo ends
if document!.isEmpty {
// if document is not empty set retVal to "true" (at this point there is no-one that could look at value of retVal)
retVal = "true"
print("No user with username")
}
}
// while query is evaluating in background
// print retVal (probably still "false")
print("\(retVal)")
// return retVal (probably still "false")
return retVal
}
Now let's fix your problem.
Solution could be:
class X {
private var document: <insert correct type here>? {
didSet {
// do what you want with document
}
}
func foo() {
let db = Firestore.firestore()
let newQuery = db.collection("users").whereField("userName", isEqualTo: username)
newQuery.getDocuments {
[weak self] (document, error) in // [weak self] is important!
// I have no idea on which thread firebase runs it's callback
// It's important that only one thread could modify self.document
// otherwise you will have race condition and a lot of strange behaviours
DispatchQueue.main.async {
self?.document = document;
}
}
}
}
If you really need to create func foo() -> String and you don't care that your thread will have to wait (UI will not respond, you will have 0 fps etc) you can do it using NSLock (I won't post code since it's really bad idea in most of the cases).
I'm actually trying to some code logic with swift for training purpose at the moment, I was wondering what is the proper way to throw my error from my init ?
So the flow is Controller ask for account creation when initializing Model is asking my sql manager to create the account and this method return the result from a closure.
But something feels wrong, should I just use a return from the sql manager who contained both my Int? and Error? ?
init(_ username: String, _ password: String) throws {
self.id = 0
self.username = username
self.password = password
var toThrow: Error? = nil
// Register in database
userManager.create(self) { (id: Int?, err: Error?) in
Thread.sleep(forTimeInterval: 10)
if let error = err {
// Register in database goes wrong
debugPrint("Handle error from user creation...")
toThrow = error
} else {
// There is no id and no error ?
guard let _ = id else { return }
self.id = id!
}
}
if let error = toThrow {
throw error
}
}
If you are on Swift 5 you could look into using Result and define your closure like
(id: Int) -> Result<Int, Error>
and change your code to
userManager.create(self) { (id: Int?) -> Result<Int, Error> in
Thread.sleep(forTimeInterval: 10)
if let error = err {
// Register in database goes wrong
debugPrint("Handle error from user creation...")
return .failure(error)
} else {
// There is no id and no error ?
guard let _ = id else { return }
return .success(id)
}
}
If you have your own error enum for the Db class like
enum DbError {
case create
case update
//...
}
Then you can use that type in the closure declaration
(id: Int?) -> Result<Int, DbError>
and return a specific error for this action
return .failure(.create)
Note that I haven't compiled this so consider it an example
Here is the solution if people want to see:
#IBAction func didPressRegister() {
guard let username = usernameField.text else { return }
guard let password = passwordField.text else { return }
let user = UserModel(username, password)
userManager.create(user) { result in
switch(result) {
case .failure(let error):
// TODO: UIAlert
debugPrint(error)
case .success(let int):
// TODO: Generate user token and redirect main
debugPrint(int)
}
}
}
// TODO
public func create(_ user: UserModel, _ complete: #escaping (Result<Int, Error>) -> ()) {
debugPrint("Requested to create the user... \(user)")
complete(.failure(toThrow.ACCOUNT_ERROR))
}
In a tableView I have a list of jobs. These jobs can be accessed by multiple users, therefore I need to use FIRTransaction. Based on the result of the first write to FirebaseDatabase, I need to write/not write to another path in Firebase.
The schema is as follows:
//Claim job by - Cleaner
//cleaner tries to claim booking at Users/UID/cus/bookingNumber
//if FIRMutableData contains a key ‘claimed’ or key:value “claimed”:true
//update at Users/UID/cus/bookingNumber with key:value
//based on response received write or not to another path
//write to Cleaners/UID/bookingNumber
//key:value
If the internet connection drops before client app receives response from firebase server, write to Cleaners/UID/bookingNumber will not be made.
How can I solve this problem?
#IBAction func claimJob(_ sender: Any) {
dbRef.runTransactionBlock({ (_ currentData:FIRMutableData) -> FIRTransactionResult in
//if valueRetrieved is nil abort
guard let val = currentData.value as? [String : AnyObject] else {
return FIRTransactionResult.abort()
}
self.valueRetrieved = val
guard let uid = FIRAuth.auth()?.currentUser?.uid else {
print("abort no uid line 80")
return FIRTransactionResult.abort()
}
self.uid = uid
for key in self.valueRetrieved.keys {
//unwrap value of 'claimed' key
guard let keyValue = self.valueRetrieved["Claimed"] as? String else {
print("abort line 88")
return FIRTransactionResult.abort()
}
//check if key value is true
if keyValue == "true"{
//booking already assigned show alert,stop transaction
self.alertText = "Booking already taken, please refresh table!"
self.alertActionTitle = "OK"
self.segueIdentifier = "unwindfromClaimDetailToClaim"
self.showAlert()
return FIRTransactionResult.abort()
} else {
//write the new values to firebase
let newData = self.createDictionary()
currentData.value = newData
return FIRTransactionResult.success(withValue: currentData)
}//end of else
}//end of for key in self
return FIRTransactionResult.abort()
}) {(error, committed,snapshot) in
if let error = error {
//display an alert with the error, ask user to try again
self.alertText = "Booking could not be claimed, please try again."
self.alertActionTitle = "OK"
self.segueIdentifier = "unwindfromClaimDetailToClaim"
self.showAlert()
//what if internet connection drops here or client quits app ????????????
} else if committed == true {
//write to Cleaners/UID/bookingNumber
//what if internet connection drops here or client quits app??????
self.cleanersRef.setValue(snapshot?.value)
self.alertText = "Booking claimed.Please check your calendar"
self.alertActionTitle = "OK"
self.segueIdentifier = "unwindfromClaimDetailToClaim"
self.showAlert()
}
}
}//end of claimJob button
I have a function Admin that runs asynchronously in the background.
Is there a way to make sure that the function is completed before calling the code after it?
(I am using a flag to check the success of the async operation. If the flag is 0, the user is not an admin and should go to the NormalLogin())
#IBAction func LoginAction(sender: UIButton) {
Admin()
if(bool.flag == 0) {
NormalLogin()
}
}
func Admin() {
let userName1 = UserName.text
let userPassword = Password.text
let findTimeLineData2:PFQuery = PFQuery(className: "Admins")
findTimeLineData2.findObjectsInBackgroundWithBlock { (objects: [AnyObject]?, error: NSError?) -> Void in
if !(error != nil){
for object in objects as! [PFObject] {
let userName2 = object.objectForKey("AdminUserName") as! String
let userPassword2 = object.objectForKey("AdminPassword") as! String
if(userName1 == userName2 && userPassword == userPassword2) {
//hes an admin
bool.flag = 1
self.performSegueWithIdentifier("AdminPage", sender: self)
self.UserName.text = ""
self.Password.text = ""
break;
}
}
}
}
}
You need to look into completion handlers and asynchronous programming. Here's an example of an async function that you can copy into a playground:
defining the function
notice the "completion" parameter is actually a function with a type of (Bool)->(). Meaning that the function takes a boolean and returns nothing.
func getBoolValue(number : Int, completion: (result: Bool)->()) {
if number > 5 {
// when your completion function is called you pass in your boolean
completion(result: true)
} else {
completion(result: false)
}
}
calling the function
here getBoolValue runs first, when the completion handler is called (above code) your closure is run with the result you passed in above.
getBoolValue(8) { (result) -> () in
// do stuff with the result
print(result)
}
applying the concept
You could apply this concept to your code by doing this:
#IBAction func LoginAction(sender: UIButton) {
// admin() calls your code, when it hits your completion handler the
// closure {} runs w/ "result" being populated with either true or false
Admin() { (result) in
print("completion result: \(result)") //<--- add this
if result == false {
NormalLogin()
} else {
// I would recommend managing this here.
self.performSegueWithIdentifier("AdminPage", sender: self)
}
}
}
// in your method, you pass a `(Bool)->()` function in as a parameter
func Admin(completion: (result: Bool)->()) {
let userName1 = UserName.text
let userPassword = Password.text
let findTimeLineData2:PFQuery = PFQuery(className: "Admins")
findTimeLineData2.findObjectsInBackgroundWithBlock { (objects: [AnyObject]?, error: NSError?) -> Void in
if !(error != nil){
for object in objects as! [PFObject] {
let userName2 = object.objectForKey("AdminUserName") as! String
let userPassword2 = object.objectForKey("AdminPassword") as! String
if(userName1 == userName2 && userPassword == userPassword2) {
// you want to move this to your calling function
//self.performSegueWithIdentifier("AdminPage", sender: self)
self.UserName.text = ""
self.Password.text = ""
// when your completion handler is hit, your operation is complete
// and you are returned to your calling closure
completion(result: true) // returns true
} else {
completion(result: false) // returns false
}
}
}
}
}
Of course, I'm not able to compile your code to test it, but I think this will work fine.