I have an app using FirebaseAuth to authenticate users. I do this in an AuthService class;
import Foundation
import FirebaseAuth
class AuthService: NSObject {
private override init() {}
static let shared = AuthService()
let currentUser = Auth.auth().currentUser
var uid = Auth.auth().currentUser?.uid
func checkAuthState(completion: #escaping(FirebaseAuth.User?) -> Void) {
// let listener = Auth.auth().addStateDidChangeListener { (auth, user) in
Auth.auth().addStateDidChangeListener { (auth, user) in
switch user {
case .none:
print("USER NOT FOUND IN CHECK AUTH STATE")
completion(nil)
case .some(let user):
print("USER FOUND WITH ID: \(user.uid)")
completion(user)
}
}
// Auth.auth().removeStateDidChangeListener(listener)
}
}
I then sign in a user anonymously using checkAuthState function above. Once this operation completes with some user I perform an action to switch the rootViewController.
func checkAuthState() {
authService.checkAuthState { (user) in
switch user {
case .none:
SceneDelegate.shared.rootController.showWelcomeController()
case .some(_):
SceneDelegate.shared.rootController.showTabBarController()
}
}
}
}
Here is my code within my RootViewController;
func showWelcomeController() {
let welcomeController = WelcomeController(authService: AuthService.shared)
let navigationController = UINavigationController(rootViewController: welcomeController)
addChild(navigationController)
navigationController.view.frame = view.frame
view.addSubview(navigationController.view)
navigationController.didMove(toParent: self)
currentController.willMove(toParent: nil)
currentController.view.removeFromSuperview()
currentController.removeFromParent()
currentController = navigationController
}
func showTabBarController() {
let tabBarController = TabBarController()
addChild(tabBarController)
tabBarController.view.frame = view.frame
view.addSubview(tabBarController.view)
tabBarController.didMove(toParent: self)
currentController.willMove(toParent: nil)
currentController.view.removeFromSuperview()
currentController.removeFromParent()
currentController = tabBarController
}
The above operates all as expected and shows my TabBarController, however, I then run a function in one of my ViewControllers in my TabBarController which is;
var uid = Auth.auth().currentUser?.uid
func readComments(query: Query, lastDocument: DocumentSnapshot?, completion: #escaping(Result<[CommentModel], NetworkError>) -> ()) {
guard let uid = auth.uid else { return completion(.failure(.userNotFound)) }
// Other code
}
However, I get the .userNotFound error? as it returns nil? I'm confused as checkAuthState is returning a user?
This typically means that the currentUser is not yet initialized by the time that code runs. Since restoring the user's sign-in state requires a call to the server, it is an asynchronous operation.
Any code that depends on the current user state being restored should be inside (or be called from) an addStateDidChangeListener as in your first snippet. So you may need a snippet like that in your other view controller(s) too.
Related
Right now, I have a button that presents the current user's profile page (it's a given they're logged in). Unfortunately, I have to make a call to Cloud Firestore in order to load the user's information. I know that I could use Auth.auth().currentUser to try and get information synchronously, but that wouldn't conform properly to the User model I have (which contains things like birthday, username, etc).
I'm confused on how to synchronously display a new view controller which takes in the User model to show the proper data without having to wait behind the Cloud Firestore query to finish.
Should I either save the current user's data in something like UserDefaults / CoreData or should I simply just load the current user's data after the new view controller has been presented and have placeholders for images, text, etc? Worst case seems like I should block the presentation from the button click until after the query finishes, but that seems like a bad solution.
Any help would be much appreciated! This is more of a theoretical question than a specific coding problem, so that's why I didn't include any of my code.
Below is the implementation I have done based on the comments above. The only issue with this implementation if figuring out how to update a viewModel that takes in a User model in its init (like ViewModel(user: currentUserService.user()) as it won't properly update until that init is called again.
CurrentUserService.swift
protocol CurrentUserServicing {
func user() -> User
}
final class CurrentUserService: CurrentUserServicing {
private var _user: User
private var listener: ListenerRegistration?
init(user: User) {
self._user = user
startSnapshotListener()
}
deinit {
stopSnapshotListener()
}
func user() -> User {
return _user
}
private func update(user: User) {
_user = user
}
private func startSnapshotListener() {
let db = Firestore.firestore()
let ref = db.collection("users").document(_user.id)
listener = ref.addSnapshotListener { [weak self] document, error in
if let error = error {
print(error.localizedDescription)
} else if let document = document, document.exists {
let result = Result {
try document.data(as: User.self)
}
switch result {
case .success(let user):
if let user = user {
self?.update(user: user)
}
case .failure(_):
break
}
}
}
}
private func stopSnapshotListener() {
listener?.remove()
}
}
SceneDelegate.swift
var appLaunchService: AppLaunchServicing?
var window: UIWindow?
private let bag = DisposeBag()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
guard let windowScene = (scene as? UIWindowScene) else { return }
appLaunchService = AppLaunchService()
window = UIWindow(windowScene: windowScene)
window?.rootViewController = LaunchViewController()
Auth.auth().addStateDidChangeListener { [unowned self] auth, user in
if let user = user {
self.appLaunchService?.loadCurrentUser(id: user.uid)
.subscribe(onNext: { [unowned self] user in
// If we get the user object, use DI to inject into necessary controllers
let currentUserService = CurrentUserService(user: user)
self.window?.rootViewController = RootTabBarController(currentUserService: currentUserService)
}, onError: { [unowned self] error in
// In event of error, force user to log in again
self.window?.rootViewController = LandingViewController()
})
.disposed(by: self.bag)
} else {
self.window?.rootViewController = LandingViewController()
}
}
window?.makeKeyAndVisible()
}
I'm trying to implement functionality to respond to FORCE_CHANGE_PASSWORD on my iOS app that uses AWS Cognito. I used this Stack Overflow question which references this sample code. Right now, my code opens a view controller like it's supposed to; however, once on that view controller, I can't get it to do anything. In the sample code, it seems that when you want to submit the password change request you call .set on an instance of AWSTaskCompletionSource<AWSCognitoIdentityNewPasswordRequiredDetails>, yet when I do this, the protocol function didCompleteNewPasswordStepWithError is never called. Interestingly, the other protocol function getNewPasswordDetails is called quickly after viewDidLoad and I can't tell why. I believe this shouldn't be called until the user has entered their new password, etc and should be in response to .set but I could be wrong.
My code is pretty identical to the sample code and that SO post, so I'm not sure what's going wrong here.
My relevant AppDelegate code is here:
extension AppDelegate: AWSCognitoIdentityInteractiveAuthenticationDelegate {
func startNewPasswordRequired() -> AWSCognitoIdentityNewPasswordRequired {
//assume we are presenting from login vc cuz where else would we be presenting that from
DispatchQueue.main.async {
let presentVC = UIApplication.shared.keyWindow?.visibleViewController
TransitionHelperFunctions.presentResetPasswordViewController(viewController: presentVC!)
print(1)
}
var vcToReturn: ResetPasswordViewController?
returnVC { (vc) in
vcToReturn = vc
print(2)
}
print(3)
return vcToReturn!
}
//put this into its own func so we can call it on main thread
func returnVC(completion: #escaping (ResetPasswordViewController) -> () ) {
DispatchQueue.main.sync {
let storyboard = UIStoryboard(name: "ResetPassword", bundle: nil)
let resetVC = storyboard.instantiateViewController(withIdentifier: "ResetPasswordViewController") as? ResetPasswordViewController
completion(resetVC!)
}
}
}
My relevant ResetPasswordViewController code is here:
class ResetPasswordViewController: UIViewController, UITextFieldDelegate {
#IBAction func resetButtonPressed(_ sender: Any) {
var userAttributes: [String:String] = [:]
userAttributes["given_name"] = firstNameField.text!
userAttributes["family_name"] = lastNameField.text!
let details = AWSCognitoIdentityNewPasswordRequiredDetails(proposedPassword: self.passwordTextField.text!, userAttributes: userAttributes)
self.newPasswordCompletion?.set(result: details)
}
}
extension ResetPasswordViewController: AWSCognitoIdentityNewPasswordRequired {
func getNewPasswordDetails(_ newPasswordRequiredInput: AWSCognitoIdentityNewPasswordRequiredInput, newPasswordRequiredCompletionSource: AWSTaskCompletionSource<AWSCognitoIdentityNewPasswordRequiredDetails>) {
self.newPasswordCompletion = newPasswordRequiredCompletionSource
}
func didCompleteNewPasswordStepWithError(_ error: Error?) {
DispatchQueue.main.async {
if let error = error as? NSError {
print("error")
print(error)
} else {
// Handle success, in my case simply dismiss the view controller
SCLAlertViewHelperFunctions.displaySuccessAlertView(timeoutValue: 5.0, title: "Success", subTitle: "You can now login with your new passowrd", colorStyle: Constants.UIntColors.emeraldColor, colorTextButton: Constants.UIntColors.whiteColor)
self.dismiss(animated: true, completion: nil)
}
}
}
}
Thank you so much for your help in advance and let me know if you need any more information.
I've successfully set up authentication within my app using Google Sign-In to where I am able to return a Firebase User. I am attempting to set up a Sign-In screen that is only shown when there is no authenticated Firebase User, however with my current code the Sign-In screen is always visible even though I am consistently returning an authenticated user.
I've implemented the didSignInFor function in AppDelegate
func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error?) {
// ...
if let error = error {
print(error.localizedDescription)
return
}
guard let authentication = user.authentication else { return }
let credential = GoogleAuthProvider.credential(withIDToken: authentication.idToken,
accessToken: authentication.accessToken)
// ...
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error {
print(error.localizedDescription)
return
}
let session = FirebaseSession.shared
if let user = Auth.auth().currentUser {
session.user = User(uid: user.uid, displayName: user.displayName, email: user.email)
print("User sign in successful: \(user.email!)")
}
}
}
as well as a few lines in didFinishLaunchingWithOptions that sets the isLoggedIn property of my ObservableObject FirebaseSession
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseApp.configure()
let auth = Auth.auth()
if auth.currentUser != nil {
FirebaseSession.shared.isLoggedIn = true
print(auth.currentUser?.email!)
} else {
FirebaseSession.shared.isLoggedIn = false
}
//Cache
let settings = FirestoreSettings()
settings.isPersistenceEnabled = false
GIDSignIn.sharedInstance().clientID = FirebaseApp.app()?.options.clientID
GIDSignIn.sharedInstance().delegate = self
return true
}
My ObservableObject
class FirebaseSession: ObservableObject {
static let shared = FirebaseSession()
init () {}
//MARK: Properties
#Published var user: User?
#Published var isLoggedIn: Bool?
#Published var items: [Thought] = []
var ref: DatabaseReference = Database.database().reference(withPath: "\(String(describing: Auth.auth().currentUser?.uid ?? "Error"))")
//MARK: Functions
func listen() {
_ = Auth.auth().addStateDidChangeListener { (auth, user) in
if auth.currentUser != nil {
self.isLoggedIn = true
}
if let user = user {
self.user = User(uid: user.uid, displayName: user.displayName, email: user.email)
} else {
self.user = nil
}
}
}
}
Finally, I perform my authentication check in the main view of my app here accessing FirebaseSession via my ObservedObject
struct AppView: View {
#ObservedObject var session = FirebaseSession.shared
#State var modalSelection = 1
#State var isPresentingAddThoughtModal = false
var body: some View {
NavigationView {
Group {
if session.isLoggedIn == true {
ThoughtsView()
} else {
SignInView()
}
}
}
}
}
As mentioned above my check doesn't seem to work. Even though my user is authenticated, SignInView is always visible.
How can I successfully check my user authentication each time my app loads?
UPDATE
I am now able to check authentication when the app loads, but after implementing Sohil's solution I am not observing realtime changes to my ObservableObject FirebaseSession. I want to observe changes to FirebaseSession so that after a new user signs in, the body of AppView will be redrawn and present ThoughtsView instead of SignInView. Currently I have to reload the app in order for the check to occur after authentication.
How do I observe changes to FirestoreSession from AppView?
You need to do something like this. I didn't try running this so I'm not sure if there are any typos...
class SessionStore : ObservableObject {
#Published var session: FIRUser?
var isLoggedIn: Bool { session != nil}
var handle: AuthStateDidChangeListenerHandle?
init () {
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
self.session = user
} else {
self.session = nil
}
}
}
deinit {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
}
in your component:
struct AppView: View {
#ObservedObject var session = SessionStore()
var body: some View {
Group {
if session.isLoggedIn {
...
} else {
...
}
}
}
}
Note the important thing here is that the object that is changing is #Published. That's how you will receive updates in your view.
Your problem is in accessing the objects and it's value. Means, in AppDelegate.swift file you are creating an object of FirebaseSession and assigning the values, but then in your AppView you are again creating a new object of FirebaseSession which creates a new instance of the class and all the values are replaced to default.
So, you need to use the same object throughout our application lifecycle, which can be done by defining the let session = FirebaseSession() globally or by creating a Singleton Class like below.
class FirebaseSession: ObservableObject {
static let shared = FirebaseSession()
private init () {}
//Properties...
//Functions...
}
Then you can access the shared object like this:
FirebaseSession.shared.properties
This way your assigned values will be preserved during the app lifecycle.
I don't know if it should be useful for you, but I read now your question because I was finding a solution for a similar issue for me, so I found it and I'm going to share with you.
I thought about using a delegate. So I created a protocol with the name ApplicationLoginDelegate in my AppDelegate class.
I define the protocol in this way:
protocol ApplicationLoginDelegate: AnyObject {
func loginDone(userDisplayName: String)
}
And in the AppDelegate class I define the loginDelegate:
weak var loginDelegate: ApplicationLoginDelegate?
You can call the delegate func in your didSignIn func
Auth.auth().signIn(with: credential) { (res, error) in
if let err = error {
print(err.localizedDescription)
return
} else {
self.loginDelegate?.loginDone(userDisplayName: (res?.user.displayName)!)
}
}
So in the SceneDelegate you use your delegate as I show you:
class SceneDelegate: UIResponder, UIWindowSceneDelegate, ApplicationLoginDelegate {
var window: UIWindow?
var eventsVM: EventsVM?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Set the app login delegate
(UIApplication.shared.delegate as! AppDelegate).loginDelegate = self
// Environment Objects
let eventsVM = EventsVM()
self.eventsVM = eventsVM
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
.environmentObject(eventsVM)
// Use a UIHostingController as window root view controller.
[...]
}
func loginDone(userDisplayName: String) {
guard let eventsVM = self.eventsVM else { return }
eventsVM.mainUser = User(displayName: userDisplayName)
}
So when you have updated your Environment Object, that has itself a #Publisced object (for example an User object), you receive your updates everywhere you define and call the Environment Object!
That's it!
I'm trying to link two firebase accounts, usually the user would be logged in with social media or email or anonmus account and then a user would sign in with phone. I need to link these two accounts.
parent view
a function in the parent view would be called to sign up with phone number
class welcomeView: UIViewController, FUIAuthDelegate {
//stores current user
var previosUser = FIRUser()
override func viewDidLoad() {
super.viewDidLoad()
if let user = Auth.auth().currentUser {
previosUser = user
}else{
Auth.auth().signInAnonymously() { (user, error) in
if error != nil {
//handle error
}
previosUser = user!
}
}
}
func signUpWithPhone(){
let vc = signUpWithPhoneView()
vc.authUI?.delegate = self
vc.auth?.languageCode = "ar"
self.present(vc, animated: true)
}
}
in the child view (signUpWithPhoneView) I present the FUIPhoneAuth
phoneProvider.signIn(withPresenting: self)
child view
import UIKit
import FirebaseAuthUI
import FirebasePhoneAuthUI
class signUpWithPhoneView: UIViewController {
fileprivate(set) var authUI = FUIAuth.defaultAuthUI()
var didload = false
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
if !didload { // stops from looping
didload = !didload
guard let authUI = self.authUI else {return}
let phoneProvider = FUIPhoneAuth.init(authUI: authUI)
self.authUI?.providers = [phoneProvider]
>> phoneProvider.signIn(withPresenting: self)
}
}
}
when the user signs in, the child view will be dismissed automatically and I have didSignInWith function that will be Called in the parents view. I need to link the previous user account and the user phone account
parent view
func authUI(_ authUI: FUIAuth, didSignInWith user: FirebaseAuth.User?, error: Error?) {
if let user = user{
// link the the two accounts
}else{
}
}
I tried to link by using
let provider = PhoneAuthProvider.provider(auth: authUI.auth!)
let credential = PhoneAuthProvider.credential(provider)
previosUser.link(with: credential, completion: { (user, error) in
if error != nil {
print("this is linking two acounts error : " , error)
}
})
but there was error in credential
in ...previosUser.link(with: *credential*, completion: ...
Cannot convert value of type '(String, String) -> PhoneAuthCredential' to expected
argument type 'AuthCredential'
any help would be appreciated
thanks
You are getting the provider with authUI.auth and then not using it to get the credential and you are currently using the static reference of the instance method credential.
The instance method itself takes two strings and returns an AuthCredential which is why you're seeing (String, String) -> AuthCredential. You must create the credential using a verification ID and code that you must collect from the user.
guard let auth = authUI.auth else { return }
let provider = PhoneAuthProvider.provider(auth: auth)
let credential = provider.credential(
withVerificationID: "XXXXXXXXXXXX",
verificationCode: "XXXXXX"
)
// ...
I'm setting my window's root view controller as the login controller right off the bat, and the login controller checks user authentication from Firebase. If the user is logged in, the controller changes the root view controller to be the feed controller. Otherwise, the login controller proceeds as itself.
class LoginController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let uid = Auth.auth().currentUser?.uid {
Database.database().reference().child("users").observeSingleEvent(of: .value, with: { snapshot in
if snapshot.hasChild(uid) {
AppDelegate.launchApplication()
}
else {
self.setUpUI()
}
})
}
else {
self.setUpUI()
}
}
...
}
where launchApplication is
class func launchApplication() {
guard let window = UIApplication.shared.keyWindow else { return }
window.rootViewController = UINavigationController(rootViewController: FeedController())
}
In addition to if let uid = Auth.auth().currentUser?.uid, I'm checking whether the uid (if it isn't nil) exists in my database, because I have had the situation where a deleted user still wasn't nil.
The problem is that after the launch screen finishes, there is a moment when the login controller, though blank, is visible. Sometimes this moment lasts a few seconds. How can I check authentication such that the login controller isn't visible at all—so that the app decides how to proceed immediately after the launch screen disappears? Thanks.
1) use this following code in case if you want to set a rootController from app delegate itself . use a check if your currentUser.uid is not nil and matched with values in database then perform following code in DidFinishLaunchingWithOptions of Appdelegate . Used by me
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseApp.configure()
GIDSignIn.sharedInstance().clientID = FirebaseApp.app()?.options.clientID
if let uid = Auth.auth().currentUser?.uid {
Database.database().reference().child("users").child(uid).observeSingleEvent(of: .value, with: { snapshot in
if snapshot.exist() {
let storyBoard = UIStoryboard.init(name: "Main", bundle: nil)
let vc = storyBoard.instantiateViewController(withIdentifier: "feedController") as! FeedController
self.window?.rootViewController=vc
self.window?.makeKeyAndVisible()
}
else {
//Present your loginController here
}
})
}
return true
}
2) Method: when you had initialised your logincontroller instead of calling a function from Appdelegate or code in LaunchApplication. make a function in login class and write the following code when required parameters are matched
var transition: UIViewAnimationOptions = .transitionFlipFromLeft
let rootviewcontroller: UIWindow = ((UIApplication.shared.delegate?.window)!)!
rootviewcontroller.rootViewController = self.storyboard?.instantiateViewController(withIdentifier: "rootnav")//rootnav is Storyboard id of my naviagtionController attached to the DestinationController [FeedController]
let mainwindow = (UIApplication.shared.delegate?.window!)!
mainwindow.backgroundColor = UIColor(hue: 0.6477, saturation: 0.6314, brightness: 0.6077, alpha: 0.8)
UIView.transition(with: mainwindow, duration: 0.55001, options: transition, animations: { () -> Void in
}) { (finished) -> Void in
}
As a quick fix, you could try checking just the desired "users" child node (and not the complete branch). For instance:
Database.database().reference().child("users").child(uid).observeSingleEvent(of: .value, with: { snapshot in
if snapshot.exists() { ...
This could reduce your delay considerably if your database contains many users.
If that isn't enough, you might consider moving this logic to you AppDelegate class as well and show your LoginController from there (and maybe holding off your launch screen a little longer until you find out if an user is available).
Instead of changing any code, I simply set the background of the login controller to match the launch screen:
class LoginController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// here
let background_image_view = UIImageView(image: #imageLiteral(resourceName: "launch_screen_background"))
background_image_view.frame = self.view.frame
self.view.addSubview(background_image_view)
if let uid = Auth.auth().currentUser?.uid {
Database.database().reference().child("users").observeSingleEvent(of: .value, with: { snapshot in
if snapshot.hasChild(uid) {
AppDelegate.launchApplication()
}
else {
self.setUpUI()
}
})
}
else {
self.setUpUI()
}
}
...
}
There is no noticeable transition from the launch screen to the login controller.