2

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?

Austin Berenyi
  • 1,013
  • 2
  • 13
  • 25

3 Answers3

1

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.

Sohil R. Memon
  • 9,404
  • 1
  • 31
  • 57
  • This solution works in the sense that I am now able to successfully check my authentication from `AppView`, however I'm unable to observe changes to my `ObservableObject` `FirebaseSession` from `AppView` like I need to. My goal is that once a new user signs in, `FirebaseSession.isLoggedIn` will toggle causing `AppView` to be redrawn. With your above solution I have to reload the app in order for the check to happen because `FirebaseSession` isn't being observed. – Austin Berenyi Nov 27 '19 at 16:40
1

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.

Gil Birman
  • 35,242
  • 14
  • 75
  • 119
0

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!

Pask
  • 1