Handling Firebase Auth error in SwiftUI Combine app - swift

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.

Related

How to observe login state?

I want View Controllers to be aware of every change in login status. Do I have to make a single tone and subscribe?
Singleton.swift
class Singleton {
static let shared = Singleton()
let isLogin: BehaviorRelay<Bool>
private init() {
isLogin = BehaviorRelay<Bool>(value: false)
}
}
SomeViewController
class SomeVc: UIViewController {
Sigleton.shared.isLogin.subscribe(.....)
}
No you don't need a Singleton...
Here's code I use in actual production. This code is in my application(_:didFinishLaunchingWithOptions:) method.
_ = UserDefaults.standard.rx.observe(String.self, "token")
.map { $0 ?? "" }
.filter { $0.isEmpty }
.bind(onNext: presentScene(animated: true) { _ in
LoginViewController.scene { $0.connect() }
})
When the user logs in, I save a token in UserDefaults, when the user logs out, I remove it. The above code will present my LoginViewController when the user logs out.
If any other view controller needs to track the login state of the user, they can also subscribe to the token observable.
The presentScene(animated:_:) function and scene(_:) method both come from my CLE Library

pushViewController for once

I have one UIButton which called Get Started in the welcome screen, by clicking on this button it will goes to the PhoneNumberViewController to type and click on the next button. For new users it will require to fill out some personal information in the ProfileViewController before going to the HomeViewController. Now I am struggling how can I pop that profile for once since I do not need registered users to check their information when they logout and re-login later.
Here is my code :
private func checkUser(userId: String) {
userService.getUser(Uid: userId) { (tutor) in
if let user = user,
!user.name.isEmpty && !user.email.isEmpty {
Router.route(to: .home)
} else {
let profileViewController = UIStoryboard.main.viewController(of: ProfileViewController.self)
profileViewController.isFromOnboarding = true
self.navigationController?.pushViewController(profileViewController, animated: true)
}
}
}
You can save a value in userDefault
private func checkUser(userId: String) {
let isPresented = UserDefaults.standard.bool(forKey: "isPresented")
userService.getUser(Uid: userId) { (tutor) in
if let user = user,
!user.name.isEmpty && !user.email.isEmpty {
Router.route(to: .home)
} else if !isPresented {
UserDefaults.standard.set(true, forKey: "isPresented")
let profileViewController = UIStoryboard.main.viewController(of: ProfileViewController.self)
profileViewController.isFromOnboarding = true
self.navigationController?.pushViewController(profileViewController, animated: true)
}
}
}
Create a hasPushedProfile flag in the controller which can be used to check if the profile view has already been shown or not. On the first time through the flag will be false and will then be set to be true when the profile is displayed, next time through the profile will not display and you can do something else instead.
import UIKit
class LoginController: UIViewController {
static var hasPushedProfile = false
private func checkUser(userId: String) {
userService.getUser(Uid: userId) { (tutor) in
if let user = user,
!user.name.isEmpty && !user.email.isEmpty {
Router.route(to: .home)
} else {
if hasPushedProfile == false {
hasPushedProfile = true
let profileViewController = UIStoryboard.main.viewController(of: ProfileViewController.self)
profileViewController.isFromOnboarding = true
self.navigationController?.pushViewController(profileViewController, animated: true)
} else {
// Already pushed profile, do something else...
}
}
}
}
}

ASWebAuthenticationSession in SWIFTUI

I've been trying to figure out how to integrate an ASWebAuthenticationSession (to perform login via Oauth) with SwiftUI. I can't find any documentation on the same online anywhere and was wondering if someone with more SwiftUI and iOS dev experience could tell me how I can achieve something along the lines. I currently need a system for someone to click the Login button which then opens a ASWebAuthSession and allows the user to login before redirecting them back to my app and loading another SwiftUI view.
I have in my ContentView one button whos calls this function :
func getAuthTokenWithWebLogin() {
let authURL = URL(string: "https://test-login.blabla.no/connect/authorize?scope=openid%20profile%20AppFramework%20offline_access&response_type=code&client_id=<blalblalba>&redirect_uri=https://integration-partner/post-login")
let callbackUrlScheme = "no.blabla:/oauthdirect"
webAuthSession = ASWebAuthenticationSession.init(url: authURL!, callbackURLScheme: callbackUrlScheme, completionHandler: { (callBack:URL?, error:Error?) in
// handle auth response
guard error == nil, let successURL = callBack else {
return
}
let oauthToken = NSURLComponents(string: (successURL.absoluteString))?.queryItems?.filter({$0.name == "code"}).first
// Do what you now that you've got the token, or use the callBack URL
print(oauthToken ?? "No OAuth Token")
})
// Kick it off
webAuthSession?.start()
}
But I get this error:
Cannot start ASWebAuthenticationSession without providing presentation
context. Set presentationContextProvider before calling -start.
How should I do this in SwiftUI? Any examples would be fantastic!
With .webAuthenticationSession(isPresented:content) modifier in BetterSafariView, you can easily use ASWebAuthenticationSession in SwiftUI. It handles the work related to providing presentation context.
import SwiftUI
import BetterSafariView
struct ContentView: View {
#State private var startingWebAuthenticationSession = false
var body: some View {
Button("Start WebAuthenticationSession") {
self.startingWebAuthenticationSession = true
}
.webAuthenticationSession(isPresented: $startingWebAuthenticationSession) {
WebAuthenticationSession(
url: URL(string: "https://github.com/login/oauth/authorize?client_id=\(clientID)")!,
callbackURLScheme: "myapp"
) { callbackURL, error in
print(callbackURL, error)
}
}
}
}

Swift Firebase Check if user exists

What am i doing wrong? I have a database structure like the one shown in this image.
In appleDelegate.swift i just want to check if a certain user token actually exists under the "users" node. that is, if "users" has the child currentUserID (a string token). I understand observeSingleEvent is executed asynchronously.I get this error in swift: 'Application windows are expected to have a root view controller at the end of application launch'. in "func application(_ application: UIApplication" i have this code. I also have my completion handler function below.
if let user = Auth.auth().currentUser{
let currentUserID = user.uid
ifUserIsMember(userId:currentUserID){(exist)->() in
if exist == true{
print("user is member")
self.window?.rootViewController = CustomTabBarController()
} else {
self.window?.rootViewController = UINavigationController(rootViewController: LoginController())
}
}
return true
} else {
self.window?.rootViewController = UINavigationController(rootViewController: LoginController())
return true
}
}
func ifUserIsMember(userId:String,completionHandler:#escaping((_ exists : Bool)->Void)){
print("ifUserIsMember")
let ref = Database.database().reference()
ref.child("users").observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.hasChild(userId) {
print("user exists")
completionHandler(true)
} else {
print("user doesn't exist")
completionHandler(false)
}
})
}
I would suggest moving the code out of the app delegate and into an initial viewController. From there establish if this is an existing user and send the user to the appropriate UI.
.observeSingleEvent loads all of the nodes at a given location - one use would be to iterate over them to populate a datasource. If there were 10,000 users they would all be loaded in if you observe the /users node.
In this case it's really not necessary. It would be better to just observe the single node you are interested in and if it exists, send the user to a UI for existing users.
here's the code to do that
if let user = Auth.auth().currentUser {
let ref = self.ref.child("users").child(user.uid)
ref.observeSingleEvent(of: .value, with: { snapshot in
self.presentUserViewController(existing: snapshot.exists() )
})
}
snapshot.exists will be either true if the user node exists or false if not so the function presentUserViewController would accept a bool to then set up the UI depending on the user type.

Swift Parse not displaying error message on signup

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