I'm trying to sign up users with Firebase auth. When a user signs up, I'd like them to be added to my Users collection in Firestore as well as the Users authorization section.
The createUser(withEmail: ...) method works every time. However, my db.collection("users").document(user.id).setData([..] method will only be called if I press the sign up button twice, and at that point the createUser(withEmail ...) method gets called again. Here's the relevant code
SignupViewController.swift
#IBAction func signupButtonTapped(_ sender: UIButton) {
// user: User() defined here
usersHelper.signup(user: user, password: password) { result in
// This closure is only executed on the second press
guard let user = result as? Firebase.User else {
let error = result as? Error
self.handleSignupError(error!)
return
}
self.performSegue(withIdentifier: "ShowGroupsFromSignupSegue", sender: self)
}
}
UsersHelper.Swift
func signup(user: User, password: String, completion: #escaping (_ result: Any?) -> Void) {
let userDispatchGroup = DispatchGroup()
var signupError: Error? = nil
var dbError: Error? = nil
var firebaseUser: Firebase.User? = nil
userDispatchGroup.enter()
usersDataModel.signupUser(user: user, password: password) { result in
// Completion handler
if result as? Error != nil {
signupError = result as? Error
} else {
// Got the user
firebaseUser = result as? Firebase.User
}
userDispatchGroup.leave()
}
userDispatchGroup.enter()
usersDataModel.create(user: user) { err in
// This will only execute if signUp is called twice
if let result = err as? Error {
print("Error msg: \(result.localizedDescription)")
dbError = result
}
print("!Created db user")
userDispatchGroup.leave()
}
userDispatchGroup.notify(queue: .main) {
print("!dispatch group completed successfully")
if (signupError == nil && dbError == nil) {
completion(firebaseUser)
} else {
signupError != nil ? completion(signupError) : completion(dbError)
}
}
}
UsersDataModel.swift
func signupUser(user: User, password: String, _ completion: #escaping (_ err: Any? ) -> Void) {
// Create user in Auth & create DB entry
Auth.auth().createUser(withEmail: user.email, password: password) { (authResult, err) in
if let err = err {
print("Error creating user \(err)")
completion(err)
} else {
print("User signed up successfully")
completion(authResult) // completion called with User
}
}
}
func create(user: User, _ completion: #escaping (_ result: Any?) -> Void) {
// userData dictionary created here
db.collection("users").document(user.ID).setData(userData) { err in
if let err = err {
print("There was an error creating the user \(err)")
completion(err)
} else {
print("!User created in db successfully!")
completion(nil)
}
}
}
Any help is greatly appreciated! Thank you all in advance
I've resolved the error. I ended up nesting the second network call in order to:
Get the uid from the firestore who was authenticated
Not break firestore rules about writing to the database w/o an authorized uid
My UsersHelper.swift file now looks like
func signup(user: User, password: String, completion: #escaping (_ result: Any?) -> Void) {
let userDispatchGroup = DispatchGroup()
var signupError: Error? = nil
var dbError: Error? = nil
var firebaseUser: Firebase.User? = nil
userDispatchGroup.enter()
usersDataModel.signupUser(user: user, password: password) { result in
// Completion handler
if result as? Error != nil {
// there was an error?
print("Error: \(result)")
signupError = result as? Error
} else {
// Got the user
firebaseUser = result as? Firebase.User
// Create user entry in DB
user.ID = firebaseUser!.uid
self.usersDataModel.create(user: user) { err in
// Completion handler
if let err = err as? Error {
dbError = err
}
userDispatchGroup.leave()
print("Done")
}
}
}
userDispatchGroup.notify(queue: .main) {
print("!dispatch group completed successfully")
if (signupError == nil && dbError == nil) {
completion(firebaseUser)
} else {
signupError != nil ? completion(signupError) : completion(dbError)
}
}
}
Related
After logging in with firebase Auth, I try to update the home page tableview using a delegate except I get this issue -
2020-07-16 10:58:51.078331-0700 Appname[44300:8867431] [AXRuntimeCommon] Unknown client: Appname
2020-07-16 10:58:51.084416-0700 Appname[44300:8867435] [AXRuntimeCommon] AX Lookup problem - errorCode:1100 error:Permission denied portName:'com.apple.iphone.axserver' PID:44186
Once the app loads it checks if the user is logged in on the home page with this function
func isLoggedIn() {
if Firebase.Auth.auth().currentUser == nil {
perform(#selector(handleLogout), with: nil, afterDelay: 0)
}
}
#objc func handleLogout() {
do {
try Auth.auth().signOut()
} catch let logoutError {
print("logout error", logoutError)
}
let startview = StartView()
startview.home = self
let nav = UINavigationController(rootViewController: startview)
nav.modalPresentationStyle = .fullScreen
present(nav, animated: false)
}
Then in the login page it logs the user in and runs the function from the home page but it just shows up as blank.
#objc func Login() {
Auth.auth().signIn(withEmail: EmailField.text!, password: PasswordField.text!) { [weak self] (user, error) in
guard let StrongSelf = self else {
return
}
guard let result = user, error == nil else {
print(error!._code)
self?.handleError(error!)
return
}
let user = result.user
print("logged in \(user)")
//NotificationCenter.default.post(name: NSNotification.Name(rawValue: "loadhome"), object: nil)
StrongSelf.navigationController?.dismiss(animated: true, completion: {
self?.home.loadfirstusers()
})
}
}
var home = HomePage()
It calls this function to update the user data and gets as far as printing sameunisamecourse but it doesn't call the print inside the dispatch.notify for some reason?
func SameUniSameCourse(completion: #escaping (_ success: Bool) -> Void) {
self.dispatchGroup.enter()
service.loadUniversityAndCourse { (uni, course) in
defer{ self.dispatchGroup.leave() }
let usersRef = Firestore.firestore().collection("users").order(by: "Created", descending: true).whereField("University", isEqualTo: uni).whereField("Course", isEqualTo: course)
self.dispatchGroup.enter()
usersRef.getDocuments { (snapshot, error) in
print("samecoursesameuni")
defer{ self.dispatchGroup.leave() }
if let error = error {
print(error.localizedDescription)
} else {
for document in snapshot!.documents {
let data = document.data()
//print(data)
if let dictionary = data as [String:AnyObject]? {
let Info = UserInfo(dictionary: dictionary)
if Info.uid == Auth.auth().currentUser?.uid {
//print(Info.username)
}
else {
self.sameUniSameCourse.append(Info)
//print(Info.username!)
}}}
}
}}
self.dispatchGroup.notify(queue: .main) {
print("dispatchNotifyCalled")
if self.sameUniSameCourse.isEmpty == true {
completion(false)
}
else {
self.masterArray.append(contentsOf: self.sameUniSameCourse)
self.spinner.stopAnimating()
completion(true)
}
}
}
I would like to have global func signIn that I can use inside my app but my problem is that I need to call some functions after the user is created. I thought I could use a completion handler for that but I tried it like this which gives me an error:
static func signIn(credentials: Any?, username: String, finished: () -> Void){
Auth.auth().signIn(with: credentials as! AuthCredential, completion: { (user, error) in
if error != nil {
Utilities.showErrorPopUp(labelContent: "Fehler bei Kontoerstellung", description: error!.localizedDescription)
} else {
//user was created successfully; store name, username and UID
let db = Firestore.firestore()
let userID = user!.user.uid
db.collection("users").document(userID).setData(["username": username, "uid": user!.user.uid]) { (error) in
if error != nil {
Utilities.showErrorPopUp(labelContent: "Fehler", description: error!.localizedDescription)
}
}
// generate empty "Main Wishlist"
db.collection("users").document(userID).collection("wishlists").document("Main Wishlist").setData(["name": "Main Wishlist", "listIDX": 1]) { (error) in
if error != nil {
Utilities.showErrorPopUp(labelContent: "Fehler", description: error!.localizedDescription)
}
}
// set user status to logged-in
UserDefaults.standard.setIsLoggedIn(value: true)
UserDefaults.standard.synchronize()
finished()
}
})
}
Error:
Escaping closure captures non-escaping parameter 'finished'
Before the change my function looked like this:
Auth.auth().signIn(with: credentials, completion: { (user, error) in
if error != nil {
Utilities.showErrorPopUp(labelContent: "Fehler bei Kontoerstellung", description: error!.localizedDescription)
} else {
//user was created successfully; store name, username and UID
let db = Firestore.firestore()
let userID = user!.user.uid
db.collection("users").document(userID).setData(["username": username, "uid": user!.user.uid]) { (error) in
if error != nil {
Utilities.showErrorPopUp(labelContent: "Fehler", description: error!.localizedDescription)
}
}
// generate empty "Main Wishlist"
db.collection("users").document(userID).collection("wishlists").document("Main Wishlist").setData(["name": "Main Wishlist", "listIDX": 1]) { (error) in
if error != nil {
Utilities.showErrorPopUp(labelContent: "Fehler", description: error!.localizedDescription)
}
}
// set user status to logged-in
UserDefaults.standard.setIsLoggedIn(value: true)
UserDefaults.standard.synchronize()
// stop animation
self.logoAnimation.stop()
//transition to home
self.transitionToHome()
}
})
}
As you can see in this example I am calling self.logoAnimation.stop() and self.transitionToHome().
How can I outclass the method but still call methods when the user is signed up?
If anything is unclear just let me know :)
EDIT: I added the batch write.
static func signIn(credentials: Any?, username: String, finished: #escaping (_ done: Bool) -> Void) {
guard let credentials = credentials as? AuthCredential else {
finished(false)
return
}
Auth.auth().signIn(with: credentials, completion: { (result, error) in
if let userId = result?.user.uid { // successfully signed in
let batch = Firestore.firestore().batch()
batch.setData(["username": username, "uid": userId], forDocument: Firestore.firestore().collection("users").document(userId), merge: true)
batch.setData(["name": "Main Wishlist", "listIDX": 1], forDocument: Firestore.firestore().collection("users").document(userId).collection("wishlists").document("Main Wishlist"), merge: true)
batch.commit { (error) in
if let error = error {
print(error)
// maybe sign user out and on completion call finished(false)
// whatever you do, you must call finished(false) at some point
} else {
UserDefaults.standard.setIsLoggedIn(value: true)
UserDefaults.standard.synchronize()
finished(true) // sign-in process complete
}
}
} else { // could not sign in
if let error = error {
print(error)
}
finished(false)
}
})
}
The call to this method would look like this:
signIn(credentials: credentials, username: someString, finished: { (done) in
if done { // success
...
} else { // failure
...
}
}
i have this function to change user email. I'm able to change the email and i can see it in firebase console, but when i go return to my application to see user info, i see only the old email presented.
#IBAction func saveButton(_ sender: Any) {
let currentUser = Auth.auth().currentUser
currentUser?.updateEmail(to: emailTextField.text!) { error in
if let error = error {
print(error)
} else {
self.userDocRef = self.db.collection("users").document(self.auth.currentUser!.uid)
self.userDocRef?.getDocument(completion: {(snapshot, error) in
guard snapshot != nil else { print("Error:", error!); return }
let userData = snapshot!.data()!
self.emailTextField.text = userData["email"]! as? String
})
print("CHANGED")
}
}
}
For some reason my firebase methods do things that make no sense to me. I want to login with a user and afterwards check on some data to make a decision. Both methods signIn() and getDocument() don't go beyond the curly brackets. If I set a breakpoint or step over the next point where it stops is outside the curly brackets. What am I doing wrong?
This is the whole code:
import Foundation
import Firebase
//#objc(LoginViewController)
class LoginViewController: UIViewController {
#IBOutlet weak var errorMessage: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBOutlet weak var emailField: UITextField!
#IBOutlet weak var passwordField: UITextField!
#IBAction func didTapEmailLogin(_ sender: UIButton) {
// Check if empty
guard emailField.text != nil, passwordField.text != nil else {
self.errorMessage.text = "Fields can't be empty."
return
}
// Log in
let email = emailField.text!
let password = passwordField.text!
guard login(with: email, with: password) else {
print("Login didn't work")
return
}
// Check if user has a group yet
guard userHasGroup() else {
print("Getting data didn't work")
return
}
}
func userHasGroup() -> Bool {
var succesful = true
let db = Firestore.firestore()
let userUid = Auth.auth().currentUser?.uid
let docRef = db.collection("users").document(userUid!)
docRef.getDocument { (document, _) in
if let document = document, document.exists {
// Test
print(document.data() as! [String: Any])
} else {
succesful = false
}
}
return succesful
}
func login(with email: String, with password: String) -> Bool {
var succesful = true
Auth.auth().signIn(withEmail: email, password: password) { (user, error) in
guard error == nil, user != nil else {
// There was an error.
self.errorMessage.text = "Email/password incorrect."
succesful = false
return
}
}
return succesful
}
}
This is the definition of async behaviour. The first time through your function all the code outside the closure is executed. Then when this async call to signIn returns the code inside the closure is executed.
The problem is the structure of your function. You can't reliably return a value from a function that contains a closure as the value won't be set when the function returns.
You need to change your function to use a completion handler.
I've posted a recent example of how to do this here Why aren't my Firebase Storage URLs uploading to Google Cloud Firestore?
So indeed it was an async problem.
This is how I made it work:
#IBAction func didTapEmailLogin(_ sender: UIButton) {
// Check if empty
guard emailField.text != nil, passwordField.text != nil else {
self.errorMessage.text = "Fields can't be empty."
return
}
let email = emailField.text!
let password = passwordField.text!
loginAsync(with: email, with: password) { (loginSuccesful) in
if loginSuccesful {
self.userHasGroupAsync(completionHandler: { (hasGroup) in
if hasGroup {
self.performSegue(withIdentifier: "fromLoginToHome", sender: self)
} else {
self.performSegue(withIdentifier: "fromLoginToCreateJoinGroup", sender: self)
}
})
}
}
}
func loginAsync(with email: String, with password: String, completionHandler: #escaping (Bool) -> ()) {
var succesful = true
Auth.auth().signIn(withEmail: email, password: password) {
(user, error) in
guard error == nil, user != nil else {
// There was an error.
self.errorMessage.text = "Email/password incorrect."
succesful = false
return
}
completionHandler(succesful)
}
}
func userHasGroupAsync(completionHandler: #escaping (Bool) -> ()) {
var hasGroup = false
let db = Firestore.firestore()
let userUid = Auth.auth().currentUser?.uid
let docRef = db.collection("users").document(userUid!)
docRef.getDocument { (document, _) in
if let document = document, document.exists {
let data: [String: Any] = document.data()!
let group = data["group"] as! String
if group != "" { hasGroup = true }
}
completionHandler(hasGroup)
}
}
I'm writing some code for a login page where we take a username and find the associated password. Temporarily I've said "if email exists under username, complete segue". However when I call the method getEmail which checks for email, it never seems to exit properly with a full email address. print(email) returns the right email address so I know I've retrieved it and it's correct. I never seem to make it out of the method though. Really stuck here! Heres my code:
func getEmail(name: String) -> String{
var email = ""
ref = Database.database().reference()
self.ref.child("Users").child(name).observeSingleEvent(of: .value, with: { (snapshot) in
if let user = snapshot.value as? [String:Any] {
print("email retrieved");
email = user["email"] as! String;
print(email)
return;
}
else{
print("email could not be retrieved from the user.");
}
}){ (error) in
print("Could not retrieve object from database because: ");
print((Any).self);
}
return email;
}
func validate(){
if(Username.text == ""){
EmptyStringAlert();
}
let email = getEmail(name: Username.text!);
print(email)
if(email == ""){
return;
}
performSegue(withIdentifier: "LoginSuccess", sender: nil)
}
The call to Firebase is asynchronous, so you have to use completion in your function to get the data. Try something like this:
func getEmail(name: String, completion: #escaping (Bool, Any?, Error?) -> Void) {
var email = ""
ref = Database.database().reference()
self.ref.child("Users").child(name).observeSingleEvent(of: .value, with: { (snapshot) in
if let user = snapshot.value as? [String:Any] {
email = user["email"] as! String
completion(true, email, nil)
}
else {
completion(false, nil, nil)
}
}){ (error) in
completion(false, nil, error)
}
}
func validate(){
if(Username.text == ""){
EmptyStringAlert();
}
getEmail(name: Username.text!) { (success, response, error) in
guard success, let email = response as? String else {
print(error ?? "Failed getEmail..")
return
}
if(email == "") {
return
}
performSegue(withIdentifier: "LoginSuccess", sender: nil)
}
}