Checking user authentication using Google Sign In and SwiftUI - ios

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!

Related

Current Firebase User Returning Nil

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.

How should I save / load the current user's data from Firestore without blocking the presentation of a new view controller?

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()
}

Observable property assigned in SceneDelegate turns to nil

In my SceneDelegate I assign the variable of UserInfo() token:
SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
#ObservedObject var userInfo = UserInfo()
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
InstanceID.instanceID().instanceID { (result, error) in
if let error = error {
print("ERROR fetching remote instance ID: \(error)")
} else if let result = result {
print("REMOTE instance ID token: \(result.token)")
self.userInfo.token = result.token
}
}
Model.swift
class UserInfo: ObservableObject {
#Published var token: String? = nil{
didSet(newValue){
print("NEW value:\(newValue)")
}
}
}
print("NEW value:\(newValue)") then successfully prints the new value. However, when I access token in another model, it is nil:
class Search: ObservableObject {
let db = Firestore.firestore()
lazy var functions = Functions.functions()
let settings = UserSettings()
#ObservedObject var userInfo = UserInfo()
#ObservedObject var locationManager = LocationManager()
#Published var status = Status.offline
func getUserData() -> [String:Any?]? {
guard let birthday = UserDefaults.standard.string(forKey: "birthday"), let latitude = locationManager.location?.coordinate.latitude, let longitude = locationManager.location?.coordinate.longitude else {
print("BDAY \(UserDefaults.standard.string(forKey: "birthday"))")
print("LOCATION \(locationManager.location?.coordinate.latitude)")
print("TOKEN \(self.userInfo.token)") // prints nil
return nil
}
guard let fcmToken = self.userInfo.token else {
return nil
}
Why is this? userInfo.token is only assigned once at app startup in SceneDelegate - so I'm not sure why it's value changes to nil. Unless there are multiple instances of UserInfo() - however I thought ObservableObject would make it "one source of truth'?
1st: #ObservedObject has sense only within SwiftUI View, so just remove that wrapper from your classes.
2nd: Your SceneDelegate and Search create different instances of UserInfo, so it is obvious that changing one of them does not affect anyhow another one.
Solution: it is not clear from provided snapshot how Search can refer to SceneDelegate, but you have to inject SceneDelegate.userInfo into Search somehow in place of creation latter (either by constructor argument or by assigning property)

How do I rerender my view in swiftUI after a user logged in with Google on Firebase?

I'm fairly new to iOS Programming and swiftUI in particular.
I have the issue that I try to integrate firebase auth in my App in order to manage users.
Now the login, and log out basically works, the issue is, that after logging in, my view (which conditionally renders eighter the Google sign-in button or a list of content does not rerender so I still see the sign-in button even though I'm signed in).
I have set an observable Object to hold my auth status but unfortunately, it does not reload the current user automatically. So I set up a function to reload it manually which I would like to trigger when logging in. This works for the logout button but the logging in finishes in the AppDelegate, where for some reason I can't access the reloadUser() function.
I'm sure there is a better way to do this and would appreciate any help!
The Environment:
final class UserData: ObservableObject {
#Published var showFavoritesOnly = false
#Published var qrTags = qrTagData
#Published var user: User? = Auth.auth().currentUser
func reloadUser() -> Void {
self.user = Auth.auth().currentUser
}
}
The View I'd like to render:
struct MyQuaggsList: View {
#EnvironmentObject private var userData: UserData
var body: some View {
Group {
if getLogInState() != nil {
VStack {
NavigationView {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Show Favorites Only")
}
ForEach(userData.qrTags) { qrTag in
if !self.userData.showFavoritesOnly || qrTag.isFavorite {
NavigationLink(
destination: QuagDetail(qrTag: qrTag)
.environmentObject(self.userData)
) {
QuaggRow(qrTag: qrTag)
}
}
}
}
.navigationBarTitle(Text("My Quaggs"))
}
SignOutButton()
}
} else {
SignInView()
}
}.onAppear(perform: {self.userData.reloadUser()})
}
func getLogInState() -> User? {
return Auth.auth().currentUser
}
}
Also, note there is the .onAppear() function which unfortunately only triggers on the initial appear not on the reappearance of the view after the user logged in.
Thanks so much in advance! It has been really frustrating.
The firebase and swiftUI combination is kinda tricky at first, but you will figure out that the same pattern is used in every single project, no worries.
Just follow my steps and customise on your project, here is our strategy.
- This might be a long answer, but i want to leave it as a refrence to all Firebase-SwiftUI user Managing in Stack OverFlow. -
Creating a SessionStore class which provides the BindableObject, and listen to your users Authentification and Handle the Auth and CRUD methods.
Creating a Model to our project ( you already did it)
Adding Auth methods in SessionStore Class.
Listening for changes and putting things together.
Let s start by SessionStore Class:
import SwiftUI
import Firebase
import Combine
class SessionStore : BindableObject {
var didChange = PassthroughSubject<SessionStore, Never>()
var session: User? { didSet { self.didChange.send(self) }}
var handle: AuthStateDidChangeListenerHandle?
func listen () {
// monitor authentication changes using firebase
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
// if we have a user, create a new user model
print("Got user: \(user)")
self.session = User(
uid: user.uid,
displayName: user.displayName
)
} else {
// if we don't have a user, set our session to nil
self.session = nil
}
}
}
// additional methods (sign up, sign in) will go here
}
Notice that we’ve declared that our session property is an optional User type, which we haven’t yet defined. Let’s quickly make one:
class User {
var uid: String
var email: String?
var displayName: String?
init(uid: String, displayName: String?, email: String?) {
self.uid = uid
self.email = email
self.displayName = displayName
}
}
Now, adding signUp, signIn and signOut methods
class SessionStore : BindableObject {
// prev code...
func signUp(
email: String,
password: String,
handler: #escaping AuthDataResultCallback
) {
Auth.auth().createUser(withEmail: email, password: password, completion: handler)
}
func signIn(
email: String,
password: String,
handler: #escaping AuthDataResultCallback
) {
Auth.auth().signIn(withEmail: email, password: password, completion: handler)
}
func signOut () -> Bool {
do {
try Auth.auth().signOut()
self.session = nil
return true
} catch {
return false
}
}
}
Finally, we need a way to stop listening to our authentication change handler.
class SessionStore : BindableObject {
// prev code...
func unbind () {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
}
Finally, Making our content view:
import SwiftUI
struct ContentView : View {
#EnvironmentObject var session: SessionStore
var body: some View {
Group {
if (session.session != nil) {
Text("Hello user!")
} else {
Text("Our authentication screen goes here...")
}
}
}
}
#Ghazi Tozri thanks again for your answer, while it wasn't what I wanted to do exactly it pushed me in the right direction.
I just want to put here what I finally did so if anyone wants to not use Email / Password sign-in but Google sign-in they can benefit from it too.
I used the Combine Framework + the #Publisher Syntax to make it a bit more readable and I also don't need the Signing in and out Methods because Google Provides them.
The SwiftUI Button for Google sign-in would look something like this:
struct GoogleSignIn : UIViewRepresentable {
#EnvironmentObject private var userData: SessionStore
func makeUIView(context: UIViewRepresentableContext<GoogleSignIn>) -> GIDSignInButton {
let button = GIDSignInButton()
button.colorScheme = .dark
GIDSignIn.sharedInstance()?.presentingViewController = UIApplication.shared.windows.last?.rootViewController
//If you want to restore a session
//GIDSignIn.sharedInstance()?.restorePreviousSignIn()
return button
}
func updateUIView(_ uiView: GIDSignInButton, context: UIViewRepresentableContext<GoogleSignIn>) {
}
}
And the Used SessionStore like this:
import SwiftUI
import Combine
import Firebase
import GoogleSignIn
final class SessionStore: ObservableObject {
#Published var showFavoritesOnly = false
#Published var qrTags = qrTagData
#Published var session: User?
var handle: AuthStateDidChangeListenerHandle?
func listen() {
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
self.session = user
} else {
self.session = nil
}
}
}
}
In a view that checks for the authentication state I use the .onAppear() function like this:
struct UserProfile: View {
#EnvironmentObject private var session: SessionStore
var body: some View {
VStack {
if session.session != nil {
SignOutButton()
} else {
SignInView()
}
}.onAppear(perform: {self.session.listen()})
}
}
To sign out this function will do (from the Firebase Docs):
func signOut() -> Void {
let firebaseAuth = Auth.auth()
do {
try firebaseAuth.signOut()
}
catch let signOutError as NSError {
print ("Error signing out: %#", signOutError)
}
}
Hope this can help somebody else too.

SwiftUI A View.environmentObject(_:) for may be missing as an ancestor of this view.: file

I'm building my first app on IOS using SwiftUI and Firebase for authentication and storage
For login i use the default Firebase UI which is customized through a subclass of the FUIAuthPickerViewController called MyFUIAuthPickerViewController as detailed in https://firebase.google.com/docs/auth/ios/firebaseui
The defaultUI is initialized and shown in the scene delegate file.
// Create the SwiftUI view that provides the window contents.
//let contentView = ContentView()
self.authUI = _createAuthUI()
guard self.authUI != nil else {
print("No authUI")
return
}
self.authUI?.delegate = self
self.authUI?.shouldHideCancelButton = true
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
//window.rootViewController = UIHostingController(rootView: contentView)
window.rootViewController = UINavigationController(rootViewController: MyFUIAuthPickerViewController(authUI: authUI!))
self.window = window
window.makeKeyAndVisible()
}
The MyFUIAuthPickerViewController subclass contains not a lot at the moment but will be used to add a default background to the authorization screen
import Foundation
import FirebaseUI
import Firebase
class MyFUIAuthPickerViewController: FUIAuthPickerViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
For tracking if a user is logged in i use an Observable object called Sessionstore. The code i adapted from https://benmcmahen.com/authentication-with-swiftui-and-firebase/ which was using the old style Bindable protocol
import Foundation
import SwiftUI
import Firebase
import Combine
class SessionStore : ObservableObject
{
#Published var user: AppUser?
var handle: AuthStateDidChangeListenerHandle?
func listen () {
// monitor authentication changes using firebase
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
// if we have a user, create a new user model
print("Got user: \(user) \(user.displayName!)")
self.user = AppUser(uid: user.uid,displayName: user.displayName, email: user.email)
} else {
// if we don't have a user, set our session to nil
self.user = nil
}
}
}
func signOut () -> Bool {
do {
try Auth.auth().signOut()
print("signed out")
self.user = nil
print("user object set to nil")
return true
} catch {
print("Problem encountered signing the user out")
return false
}
}
}
The environment object is present on my contentview and my scenedelegate
scenedelegate
class SceneDelegate: UIResponder, UIWindowSceneDelegate, FUIAuthDelegate{
var window: UIWindow?
var authUI: FUIAuth?
#EnvironmentObject var appSession: SessionStore
contentview
import SwiftUI
import FirebaseUI
struct ContentView: View {
#EnvironmentObject var session: SessionStore
var body: some View {
Group{
if session.user != nil {
Text("Welcome \(session.user!.displayName!)")
} else {
Text("Please login")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
let nav = UINavigationController()
static var previews: some View {
ContentView().environmentObject(SessionStore())
}
}
In an extension on my sceneDelegate i implement the needed firebase functions.
On succesfull login i create a new appuser object which i place in the sessionStore and then change the rootviewcontroller to the contentview passing in the environmentObject
Extension SceneDelegate {
func authUI(_ authUI: FUIAuth, didSignInWith user: User?, error: Error?) {
guard user != nil else {
print("No User")
return
}
print(user!.displayName!)
let user = AppUser(uid: user!.uid,displayName: user?.email,email: user?.displayName)
self.appSession.user = user
let contentView = ContentView()
self.window?.rootViewController = UIHostingController(rootView: contentView.environmentObject(SessionStore()))
self.window?.makeKeyAndVisible()
}
func authPickerViewController(for authUI: FUIAuth) -> FUIAuthPickerViewController {
return MyFUIAuthPickerViewController(authUI: authUI)
}
}
Now when i test my app i get following error after entering my username and password
Fatal error: No ObservableObject of type SessionStore found.
A View.environmentObject(_:) for SessionStore may be missing as an ancestor of this view.: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/Monoceros_Sim/Monoceros-39.4.3/Core/EnvironmentObject.swift, line 55
I suspect this has to do with the fact that de environmentObject is lost in MyFUIAuthPickerViewController between the flow from my sceneDelegate to the ContentView but how do i prevent this from happening ? I need to somehow extend the MyFUIAuthPickerViewController to allow passing of the environmentObject but how ?
Hope my problem is clear and you guys can help.
Your code in the SceneDelegate is let contentView = ContentView() I think it should be something like let contentView = ContentView().environmentObject(SessionStore())
It also seems that you SessionStore is missing var didChange = PassthroughSubject<SessionStore, Never>()
The first lines of your SessionStore should be something like:
import Foundation
import SwiftUI
import Firebase
import Combine
class SessionStore : ObservableObject
{
#Published var user: AppUser? { didSet { self.didChange.send(self) }}
var didChange = PassthroughSubject<SessionStore, Never>()
var handle: AuthStateDidChangeListenerHandle?
You want to make sure that changes are propagating to listeners (subscribers).
And if I'm correct #EnvironmentObject var appSession: SessionStore should not be mentioned in the SceneDelegate

Resources