I've created a user class which has a Parse PFUser() object in it and a string for error_messages.
In the user class is a signup function which will use the PFUser object and perform signUpInBackgroundWithBlock().
In that function it should set a flag notifying the main view controller that an error occurred if one does as well as set the error_message string in the User object with the error message passed back from PFUser.
However what happens is the function doesn't finish executing once an error occurs for example if an incorrect email format is entered such as aaaa.com instead of aaa#a.com the function won't return and set the flag instead the error message passed from PFUser is just displayed in the console window.
I've spent a few days now trying everything imaginable to set the flag and the error message in the user class but I can't figure it out.
Here is the class code
class User {
var user_db_obj = PFUser()
var error_message = "Please try again later"
//var user_name: String
//var pass_word: String
//Constructor for User object
init(user: String, pass: String){
user_db_obj.username = user
user_db_obj.email = user
user_db_obj.password = pass
}
//This function signs a user up to the database
func signUp() -> Bool {
var error_in_sign_up: Bool = true
user_db_obj.signUpInBackgroundWithBlock {(succeeded: Bool?, error: NSError?) -> Void in
//stop spinner and allow interaction events from user
activityIndicator.stopAnimating()
UIApplication.sharedApplication().endIgnoringInteractionEvents()
if error == nil{
error_in_sign_up = false
//sign up successful
}
else{
let error_string = error!.userInfo["error"] as? String
self.error_message = error_string!
}
}
if error_in_sign_up == true{
return false
}
else{
return true
}
}
Here is the view controller code that calls the signup function from User class.
//Action for signup button
#available(iOS 8.0, *)
#IBAction func signup_btn(sender: AnyObject) {
if email_tf.text!.isEmpty || pass_tf.text!.isEmpty {
//if email or password field blank display error message
displayAlert("Error in form", msg: "Please enter a username and password")
}
else{ //perform actual signup/login if email and password supplied
//Display spinner while database interaction occuring and ignore user interactions as well
activityIndicator = UIActivityIndicatorView(frame: CGRectMake(0, 0, 50, 50))
activityIndicator.center = self.view.center
activityIndicator.hidesWhenStopped = true
activityIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.Gray
view.addSubview(activityIndicator)
activityIndicator.startAnimating()
UIApplication.sharedApplication().beginIgnoringInteractionEvents()
let theUser = User(user: email_tf.text!, pass: pass_tf.text!)
//sign up
if signup_mode == true{
if theUser.signUp() == false{
displayAlert("Failed SignUp", msg: theUser.error_message)
}
}
//login
else{
if theUser.login() == false{
displayAlert("Failed Login", msg: theUser.error_message)
}
}
}
}
the problem is that function signUpInBackgroundWithBlock doesnt run on mainthread, if you want to keep this functions you would have to register notification and then listen when it is successful or not in the other viewController... something like this
Related
I have an app where users can sign up and login using Firebase. However, I can not seem to alert the user of any errors in the view.
First we have a UserStore which is a ObservableObject and is initialised as an EnvironmentObject when setting up the view in the SceneDelegate.
let appView = AppView().environmentObject(userStore)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: appView)
self.window = window
window.makeKeyAndVisible()
}
Then we sign up or login to the View like so.
In View
self.userStore.logIn(email: self.email, password: self.password)
self.isLoggingIn = true
if self.userStore.failedToLogin {
self.isLoggingIn = false
self.alertTitle = "There seems to be a problem"
self.alertBody = self.userStore.errorMessage
self.showingAlert = true
}
Then the actual method should update the UserStore property values which then update the view and display an alert, however, this is not the case.
SignIn
// Session Properties
#Published var isLoggedIn = false {didSet{didChange.send() }}
#Published var isNewUser = false {didSet{didChange.send() }}
#Published var failedToCreateAccount = false {didSet{didChange.send() }}
#Published var failedToLogin = false {didSet{didChange.send() }}
#Published var errorMessage = "" {didSet{didChange.send() }}
init () {
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
self.session = user
self.isLoggedIn = true
//Change isNewUser is user document exists?
} else {
self.session = nil
self.isLoggedIn = false
}
}
}
func logIn(email: String, password: String) {
Auth.auth().signIn(withEmail: email, password: password) { [weak self] user, error in
print("Signed In")
if(error != nil) {
print("Failed to login")
self!.failedToLogin = true
self!.errorMessage = ("\(String(describing: error))")
print(error!)
return
} else if error == nil {
print("Success Logging In")
}
}
}
The AppView determines which view is loaded depending if the user is logged in.
AppView
if !userStore.isLoggedIn {
LoginView().transition(.opacity)
}
if userStore.isLoggedIn {
ContentView().transition(.opacity)
}
Atm error messages are not shown; the login view is also shown shortly before the main view.
How can I correctly display error messages in the view ?
The Firebase APIs are asynchronous, simply because they access a remote system, across the internet, which takes a little time. The same applies for accessing the local disk, by the way. This blog post explains this in more detail.
Consequently, userStore.login is an asynchronous process. I.e. the call to if self.userStore.failedToLogin is executed before userStore.login returns. Thus, userStore.failedToLogin is still false, and the code in the conditional statement will never be executed.
There are two ways around this:
Implement a trailing closure on userStore.logIn, and move the code which displays the error into the closure
Make userStore.failedToLogin a publisher and subscribe the visibility of your alert to it
Use Combine Firebase: https://github.com/rever-ai/CombineFirebase
This is combine-swiftui wrapper around firebase api, so you can use swift-ui publisher pattern for firebase.
I know different variations of this question have been asked. However I seem to keep running into the same issue every time.
I want to check if an email already exist before the user pushes onto the next view. I will enter an email that exist in the database and the performSegue func is always called and pushes the user as if that email does not exist.
The only way I can check officially is when the user reaches the final sign up VC and the Auth.auth().createUser(withEmail: email as! String, password: password as! String ) { (user, error) in code will check for all errors.
However for good user experience I would hate for the user to have to click back three times to change the email address. Here is the code I have for the enter email view controller.
// Check if email is already taken
Auth.auth().fetchSignInMethods(forEmail: emailTextField.text!, completion: { (forEmail, error) in
// stop activity indicator
self.nextButton.setTitle("Continue", for: .normal)
self.activityIndicator.stopAnimating()
if let error = error {
print("Email Error: \(error.localizedDescription)")
print(error._code)
self.handleError(error)
return
} else {
print("Email is good")
self.performSegue(withIdentifier: "goToCreateUsernameVC", sender: self)
}
})
First off am I even entering the create property in the forEmail section? I added emailTextField.text because its the only way I know how even get the email the user typed. Does anyone know a better way I can do this?
How I create user accounts
This is an example of what I use. When a user provides credentials, FirebaseAuth checks if these credentials can be used to make a user account. The function returns two values, a boolean indicating whether the creation was successful, and an optional error, which is returned when the creation is unsuccessful. If the boolean returns true, we simply push to the next view controller. Otherwise, we present the error.
func createUserAcct(completion: #escaping (Bool, Error?) -> Void) {
//Try to create an account with the given credentials
Auth.auth().createUser(withEmail: emailTextField.text!, password: passwordConfirmTextField.text!) { (user, error) in
if error == nil {
//If the account is created without an error, then we will make a ProfileChangeRequest, i.e. update the user's photo and display name.
if let firebaseUser = Auth.auth().currentUser {
let changeRequest = firebaseUser.createProfileChangeRequest()
//If you have a URL for FirebaseStorage where the user has uploaded a profile picture, you'll pass the url here
changeRequest.photoURL = URL(string: "nil")
changeRequest.displayName = self.nameTextField.text!
changeRequest.commitChanges { error in
if let error = error {
// An error happened.
completion(false, error)
} else {
//If the change is committed successfully, then I create an object from the credentials. I store this object both on the FirebaseDatabase (so it is accessible by other users) and in my user defaults (so that the user doesn't have to remotely grab their own info
//Create the object
let userData = ["email" : self.emailTextField.text!,"name": self.nameTextField.text!] as [String : Any]
//Store the object in FirebaseDatabase
Database.database().reference().child("Users").child(firebaseUser.uid).updateChildvalues(userData)
//Store the object as data in my user defaults
let data = NSKeyedArchiver.archivedData(withRootObject: userData)
UserDefaults.standard.set(data, forKey: "UserData")
UserDefaults.standard.set([Data](), forKey: "UserPhotos")
completion(true, nil)
}
}
}
} else {
// An error happened.
completion(false, error)
}
}
}
Here is an example of how I would use it. We can use the success boolean returned to determine if we should push to the next view controller, or present an error alert to the user.
createUserAcct { success, error in
//Handle the success
if success {
//Instantiate nextViewController
let storyboard = UIStoryboard(name: "Main", bundle: .main)
let nextVC = storyboard.instantiateViewController(withIdentifier: "NextVC") as! NextViewController
//Push typeSelectVC
self.navigationController!.pushViewController(viewController: nextVC, animated: true, completion: {
//We are no longer doing asynchronous work, so we hide our activity indicator
self.activityIndicator.isHidden = true
self.activityIndicator.stopAnimating()
})
} else {
//We now handle the error
//We are no longer doing asynchronous work, so we hide our activity indicator
self.activityIndicator.isHidden = true
self.activityIndicator.stopAnimating()
//Create a UIAlertController with the error received as the message (ex. "A user with this email already exists.")
let alertController = UIAlertController(title: "Error", message: error!.localizedDescription, style: .alert)
let ok = UIAlertAction(title: "OK", style: .cancel, action: nil)
//Present the UIAlertController
alertController.addAction(ok)
self.present(alertController, animated: true, completion: nil)
}
}
Let me know if this all makes sense, I know there is a lot to it. I'm just considering things you'll maybe find you need done anyways that you may not be aware of (like making change requests, or storing a data object on FirebaseDatabase).
Now for checking if the email is already taken:
Remember when I said that I post a user object to FirebaseDatabase upon account creation? Well we can query for the given email to see if it already exists. If it doesn't we continue with the flow as normal, without having actually created the account. Otherwise, we simply tell the user to pick another email address.
Pushing a user object to your database (taken from the above code):
if let firebaseUser = Auth.auth().currentUser {
//Create the object
let userData = ["email" : self.emailTextField.text!,"name": self.nameTextField.text!] as [String : Any]
//Store the object in FirebaseDatabase
Database.database().reference().child("Users").child(firebaseUser.uid).updateChildvalues(userData)
}
And now querying to see if somebody already has that email:
func checkIfEmailExists(email: String, completion: #escaping (Bool) -> Void ) {
Database.database().reference().child("Users").queryOrdered(byChild: "email").queryEqual(toValue: email).observeSingleEvent(of: .value, with: {(snapshot: DataSnapshot) in
if let result = snapshot.value as? [String:[String:Any]] {
completion(true)
} else {
completion(false)
}
}
}
Then we can call this like so:
checkIfEmailExists(email: emailTextField.text!, completion: {(exists) in
if exists {
//Present error that the email is already used
} else {
//Segue to next view controller
}
})
In my Xcode Project I already have a sign in feature that uses a user's username and password they created. Now I want to integrate a Facebook login in the project, but I am not completely sure how to do this. When someone makes an account through they way I have it now, they make and save a username and a password which can then be used in with PFUser.logIn(withUsername: ..., password: ...) for whenever they want to sign in again.
But with Facebook, I do not know what unique identifier I am suppose to save their account with. I know there is a Facebook id that I can extract, but how do I login the user in after I get this? Like I said before I currently use PFUser.logIn(withUsername: ..., password: ...) to log the users' in then I just use PFUser.current()?.username to get all data related to that user. I heard that I am suppose to use PFFacebookUtils.logInInBackground for this, but I already tried implementing it but when I press the "Continue with Facebook button", the app crashes (I screenshot error and placed it at bottom). I have copied my code of what I have so far. How do I integrate a signup with Facebook feature that will allow me to save a users Facebook id and what method do I use to sign the user in (i.e. PFFacebookUtils.logInInBackground)? Here's my code so far:
#IBOutlet var facebookSignUpButton: FBSDKLoginButton!
var fullnameFB = String()
var idFB = String()
var emailFB = String()
var isFBSignUp = Bool()
override func viewDidLoad() {
super.viewDidLoad()
signUpWithFacebook()
}
func signUpWithFacebook() {
facebookSignUpButton.readPermissions = ["email", "public_profile"]
facebookSignUpButton.delegate = self
self.view.addSubview(facebookSignUpButton)
}
func loginButton(_ loginButton: FBSDKLoginButton!, didCompleteWith result: FBSDKLoginManagerLoginResult!, error: Error!) {
if error != nil { //if theres an error
print(error)
} else if result.isCancelled { // if user cancels the sign up request
print("user cancelled login")
} else {
PFFacebookUtils.logInInBackground(with: result!.token!) { (user, error) in
if error == nil {
if let user = user {
if user.isNew {
print("User signed up and logged in through Facebook!")
} else {
print("User logged in through Facebook!")
}
if result.grantedPermissions.contains("email") {
if let graphRequest = FBSDKGraphRequest(graphPath: "me", parameters: ["fields": "email, name"]) {
graphRequest.start(completionHandler: { (connection, result, error) in
if error != nil {
print(error?.localizedDescription ?? String())
} else {
if let userDetails = result as? [String: String]{
print(userDetails)
self.fullnameFB = userDetails["name"]!
self.idFB = userDetails["id"]!
self.emailFB = userDetails["email"]!
self.isFBSignUp = true
}
}
})
}
} else {
print("didnt get email")
self.createAlert(title: "Facebook Sign Up", message: "To signup with Facebook, we need your email address")
}
} else {
print("Error while trying to login using Facebook: \(error?.localizedDescription ?? "---")")
}
} else {
print(error?.localizedDescription ?? String())
}
}
}
}
Console output:
2017-08-12 14:14:33.223472-0700 Project[2423:1001235] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'You must initialize PFFacebookUtils with a call to +initializeFacebookWithApplicationLaunchOptions'
*** First throw call stack:
(0x188042fe0 0x186aa4538 0x188042f28 0x100b8f934 0x100b90020 0x100b9032c 0x10019eee8 0x1001a0a64 0x100867598 0x10086e998 0x10086e4f0 0x100871a94 0x18b3610d4 0x101521a10 0x101526b78 0x187ff10c8 0x187feece4 0x187f1eda4 0x189989074 0x18e1dd364 0x1001ec288 0x186f2d59c)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb)
As stated in the docs, you need to initialize PFFacebookUtils before using it, like so:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
// CODE...
PFFacebookUtils.initializeFacebook(applicationLaunchOptions: launchOptions)
}
Important: Must be done after Parse setup (Parse.initialize...).
I run the app in Xcode simulator and log in with user and password then I log out, the logOutOutlet and bookingsOutlet are hidden and logInOutlet button shows. So far it behaves as expected.
2
Next, stop the app from Xcode and run it again, but now it behaves as if I was already logged in: shows logOutOutlet,bookingsOutlet, hides logInOutlet. It behaves in the same when the app is suspended, terminated or deleted. ( i.e, Stop in Xcode or cmd + H * 2 and swipe up.)
Now, assuming that I have an active logIn session in the next ViewController I try to retrieve data from FireBase Realtime DataBase, but I get this error. Error Domain=com.firebase Code=1 "Permission Denied" UserInfo={NSLocalizedDescription=Permission Denied}. Why can I not log out completely?
#IBAction func logOut(sender: AnyObject) {
// if the current user is logged in, try to log out and if logout is successful:
// hide: logOut button & bookings button
// show: logIn button
if FIRAuth.auth() != nil {
do {
try FIRAuth.auth()?.signOut()
print("the user is logged out")
} catch let error as NSError {
print(error.localizedDescription)
print("the current user id is \(FIRAuth.auth()?.currentUser?.uid)")
}
self.logInOutlet.hidden = false
self.logOutOutlet.hidden = true
self.bookingsOutlet.hidden = true
} // end of if.. FIRAuth.auth()
}
override func viewDidLoad() {
super.viewDidLoad()
// if the user is logged out
// hide: logOut button & bookings button
// show: logIn button
// if FIRAuth.auth()?.currentUser?.uid == nil {
if FIRAuth.auth() == nil {
self.logInOutlet.hidden = false
self.logOutOutlet.hidden = true
self.bookingsOutlet.hidden = true
// it still prints the user id, why?
if let userNOTLogged = FIRAuth.auth()?.currentUser?.uid {
print("User should NOT be logged \(userNOTLogged)")
}
// if the user is logged in
// hide: logIn Button
// show: logOut button & bookings button
} else if
FIRAuth.auth() != nil {
self.logInOutlet.hidden = true
self.logOutOutlet.hidden = false
self.bookingsOutlet.hidden = false
}
}
// Log in code
FIRAuth.auth()?.signInWithEmail(email.text!, password: password.text!, completion: { (authData, error) in
let customError = error?.localizedDescription
if error != nil {
print(customError)
// display an alert with the error
self.displayAlert()
} else {
print("The user has been logged in")
//if signIn was successful, instantiate the view controller with identifier SWrevelViewidentifier
let toMenuOptions = self.storyboard?.instantiateViewControllerWithIdentifier("SWrevelViewidentifier")
self.presentViewController(toMenuOptions!, animated: true, completion: nil)
}
})
}
I had this exact same problem. Since this happens when you relaunch, try putting this code in your app.delegate.
FIRAuth.auth()?.addAuthStateDidChangeListener { auth, user in
if let user = user {
// User is signed in.
} else {
// No user is signed in.
}
}
How can I validate a form after a RAC Command or a button click? I have this somewhere in my View Model
self.executeLoginRacCommand = RACCommand(enabled: activateButtonSignal, signalBlock: { (any : AnyObject!) -> RACSignal! in
return self.executeLoginAPI()
})
//My question is here, how can I display an alert to my view controller saying may email is invalid.
//MARK: - Execute Login API
private func executeLoginAPI() -> RACSignal {
if self.isEmailValid(self.email) {
return self.signInServices.signInAPIService.signInAPISignal(self.email, password: self.password)
} else {
self.errorMessage = "Please insert a valid email"
return RACSignal.empty()
}
}
//MARK: - Is Email Valid
func isEmailValid(email: String) -> Bool {
let emailRegEx = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+#[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
let emailTest = NSPredicate(format:"SELF MATCHES %#", emailRegEx)
return emailTest.evaluateWithObject(email)
}
I access it in my View Controller like this:
self.signInButton.rac_command = self.signInViewModel.executeLoginRacCommand
Can you please suggest a good way of validating an invalid email and display alert after a button click? Thanks in advance!
In your else branch, you are returning an empty RACSignal.
Instead, return the a RACSignal that sends the error (RACSignal.error(<the error>)).
In your ViewController, you can subscribe to the errors property of self.executeLoginRacCommand and display the error to the user.