NSInternalInconsistencyException: Firebase crashes SwiftUI app on launch - ios

I am trying to add Firebase auth via Apple Sign-in and have got this error:
crashed due to an uncaught exception `NSInternalInconsistencyException`. Reason: The default FirebaseApp instance must be configured before the default Authinstance can be initialized. One way to ensure this is to call `FirebaseApp.configure()` in the App Delegate's `application(_:didFinishLaunchingWithOptions:)` (or the `#main` struct's initializer in SwiftUI)
This is how I launch Firebase:
import Firebase
import SwiftUI
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
FirebaseApp.configure()
return true
}
}
I did not install the Firebase package directly, but instead added this package. which I believe added Firebase as one of its dependency. I have checked other solutions but they all rely on CocoaPods while I'm using the Swift Package Manager. Not sure how I can resolve this error.
App calling AppDelegate:
import FirebaseService
import SwiftUI
#main
struct SomeApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#State private var authState = AuthState()
var body: some Scene {
WindowGroup {
switch authState.value {
case .undefined:
ProgressView()
case .authenticated:
ContentView()
case .notAuthenticated:
AuthView()
}
}
}
}

This error message indicates that another part of your app tries to access Firebase Auth before Firebase is initialised in your your app delegate.
Looking at the code of Rebeloper's package which you're using, we can see that it calls Auth.auth().addStateDidChangeListener in line 16 of AuthListener. Since you create an instance of AuthState (another class from Rebeloper's library), this is executed before Firebase is initialised.
Instead of using an undocumented third party library, I would recommend implementing this yourself - especially since it's only a few lines of code.
To see how this is done, check out Getting started with Firebase Auth for Apple platforms - Firebase Fundamentals, which not only explains Firebase Auth and how to handle authentication state in detail, but also includes the corresponding source code.
As an added bonus, this contains a beautiful signup/login form:
For reference, here is the source code for registering an authentication state listener in an ObserableObject:
#MainActor
class AuthenticationViewModel: ObservableObject {
#Published var email = ""
#Published var password = ""
#Published var confirmPassword = ""
#Published var flow: AuthenticationFlow = .login
#Published var isValid = false
#Published var authenticationState: AuthenticationState = .unauthenticated
#Published var errorMessage = ""
#Published var user: User?
#Published var displayName = ""
init() {
registerAuthStateHandler()
// ...
}
private var authStateHandler: AuthStateDidChangeListenerHandle?
func registerAuthStateHandler() {
if authStateHandler == nil {
authStateHandler = Auth.auth().addStateDidChangeListener { auth, user in
self.user = user
self.authenticationState = user == nil ? .unauthenticated : .authenticated
self.displayName = user?.email ?? ""
}
}
}
// ...
}

Related

How do you extract App Delegate data in a SwiftUI project?

I have a SwiftUI-Firebase Project and I'm using an App Delegate to handle push notifications. The client side generates a cloud messsaging registration token that I'd like to store in firestore along with the userID and other meta data.
extension AppDelegate: MessagingDelegate {
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
tokenData = [
"token": fcmToken ?? ""
]
// Store token in firestore for sending notifications from server in future...
print("dataDict: \(tokenData)")
}
}
So, I need to get the tokenData, user metadata, package it into a custom object, and finally store it in firestore. I'm doing this by first creating a tokenModel:
struct FCMToken: Codable, Identifiable {
#DocumentID var id: String? = UUID().uuidString
var userID: String?
var token: String
var createdAt: Date
enum CodingKeys: String, CodingKey {
case userID
case token
case createdAt
}
}
And then I'd like to pass tokenData from the delagate into my HomeView() so that I can test whether a user is logged in:
#main
struct erisApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
HomeView(fcmTokenData: delegate.tokenData)
}
}
}
If so, I can call a method to firestore inside HomeView() to store my tokenData. The issue is that I'm unable to extract this tokenData out of the delegate. I've pretty comfortable with SwiftUI but quite new to UIKit, Protocol-Delegate pattern, and so on. Can someone guide me on how to achieve the desired result?
Thank you.
To obtain AppDelegate properties in SwiftUI, use #UIApplicationDelegateAdaptor with the ObservableObject protocol and the #Published and #EnvironmentObject wrappers as explained in the Apple Docs here.
Add the ObservableObject protocol to your AppDelegate class. Then declare tokenData as #Published. This assumes tokenData is updated by your AppDelegate extension above.
class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
#Published var tokenData: [TokenData] = []
// application functions...
}
Add the #UIApplicationDelegateAdaptor in the App declaration, as you have done already, but modify HomeView like this:
#main
struct erisApp: App {
#UIApplicationDelegateAdaptor var delegate: AppDelegate
var body: some Scene {
WindowGroup {
HomeView()
}
}
}
Set up HomeView() with an #EnvironmentObject and then use delegate.tokenData as needed:
struct HomeView: View {
#EnvironmentObject var delegate: AppDelegate
var body: some View {
Text("Token Data: \(delegate.tokenData)")
}
}

SceneDelegate.swift deprecated/changed architecture? [duplicate]

Now that AppDelegate and SceneDelegate are removed from SwiftUI, where do I put the code that I used to have in SceneDelegate and AppDelegate, Firebase config for ex?
So I have this code currently in my AppDelegate:
Where should I put this code now?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseConfiguration.shared.setLoggerLevel(.min)
FirebaseApp.configure()
return true
}
Here is a solution for SwiftUI life-cycle. Tested with Xcode 12b / iOS 14
import SwiftUI
import UIKit
// no changes in your AppDelegate class
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print(">> your code here !!")
return true
}
}
#main
struct Testing_SwiftUI2App: App {
// inject into SwiftUI life-cycle via adaptor !!!
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Overriding the initializer in your App also works:
import SwiftUI
import Firebase
#main
struct BookSpineApp: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
BooksListView()
}
}
}
Find a more detailed write-up here:
The Ultimate Guide to the SwiftUI 2 Application Life Cycle
Firebase and the new SwiftUI 2 Application Life Cycle
You should not put that kind of codes in the app delegate at all or you will end up facing the Massive App Delegate. Instead, you should consider refactoring your code to more meaningful pieces and then put the right part in the right place. For this case, the only thing you need is to be sure that the code is executing those functions once the app is ready and only once. So the init method could be great:
#main
struct MyApp: App {
init() {
setupFirebase()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
private extension MyApp {
func setupFirebase() {
FirebaseConfiguration.shared.setLoggerLevel(.min)
FirebaseApp.configure()
}
}
AppDelegate ?
You can have your own custom class and assign it as the delegate. But note that it will not work for events that happen before assignment. For example:
class CustomDelegate: NSObject, UIApplicationDelegate {
static let Shared = CustomDelegate()
}
And later:
UIApplication.shared.delegate = CustomDelegate.Shared
Observing For Notifications
Most of AppDelegate methods are actually observing on notifications that you can observe manually instead of defining a new class. For example:
NotificationCenter.default.addObserver(
self,
selector: #selector(<#T###objc method#>),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
Native AppDelegate Wrapper
You can directly inject app delegate into the #main struct:
#UIApplicationDelegateAdaptor(CustomDelegate.self) var appDelegate
Note: Using AppDelegate
Remember that adding AppDelegate means that you are killing default multiplatform support and you have to check for platform manually.
You can also use the new ScenePhase for certain code that the AppDelegate and SceneDelegate had. Like going to the background or becoming active. From
struct PodcastScene: Scene {
#Environment(\.scenePhase) private var phase
var body: some Scene {
WindowGroup {
TabView {
LibraryView()
DiscoverView()
SearchView()
}
}
.onChange(of: phase) { newPhase in
switch newPhase {
case .active:
// App became active
case .inactive:
// App became inactive
case .background:
// App is running in the background
#unknown default:
// Fallback for future cases
}
}
}
}
Example credit: https://wwdcbysundell.com/2020/building-entire-apps-with-swiftui/
Note the method below will stop cross platform support so should only be used if you are planning on building for iOS only.
It should also be noted that this doesn’t use the SwiftUI lifecycle method, instead it allows you to return to the UIKit lifecycle method.
You can still have an AppDelegate and a SceneDelegate when you create a SwiftUI app in Xcode 12-beta.
You just need to make sure that you have chosen the correct option for the Life Cycle when you create your app.
Make sure you choose UIKit App Delegate for the Life Cycle and you will get an AppDelegate and a SceneDelegate
I would also advise in using the main App's init method for this one, as it seems safe to use (any objections?).
What I usually do, that might be useful to share, is to have a couple of utility types, combined with the Builder pattern.
/// An abstraction for a predefined set of functionality,
/// aimed to be ran once, at app startup.
protocol StartupProcess {
func run()
}
/// A convenience type used for running StartupProcesses.
/// Uses the Builder pattern for some coding eye candy.
final class StartupProcessService {
init() { }
/// Executes the passed-in StartupProcess by running it's "run()" method.
/// - Parameter process: A StartupProcess instance, to be initiated.
/// - Returns: Returns "self", as a means to chain invocations of StartupProcess instances.
#discardableResult
func execute(process: any StartupProcess) -> StartupProcessService {
process.run()
return self
}
}
and then we have some processes
struct CrashlyticsProcess: StartupProcess {
func run() {
// Do stuff, like SDK initialization, etc.
}
}
struct FirebaseProcess: StartupProcess {
func run() {
// Do stuff, like SDK initialization, etc.
}
}
struct AppearanceCustomizationProcess: StartupProcess {
func run() {
// Do stuff, like SDK initialization, etc.
}
}
and finally, running them
#main
struct TheApp: App {
init() {
initiateStartupProcesses()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
private extension TheApp {
func initiateStartupProcesses() {
StartupProcessService()
.execute(process: ExampleProcess())
.execute(process: FirebaseProcess())
.execute(process: AppearanceCustomizationProcess)
}
}
Seems quite nice and super clean.
I see a lot of solutions where init gets used as didFinishLaunching. However, didFinishLaunching gets called AFTER init of the App struct.
Solution 1
Use the init of the View that is created in the App struct. When the body of the App struct gets called, didFinishLaunching just happened.
#main
struct MyApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#ViewBuilder
var body: some Scene {
WindowGroup {
MainView(appDelegate: appDelegate)
}
}
}
struct MainView: View {
init(appDelegate: AppDelegate) {
// at this point `didFinishLaunching` is completed
setup()
}
}
Solution 2
We can create a block to notify us when didFinishLaunching gets called. This allows to keep more code in SwiftUI world (rather than in AppDelegate).
class AppDelegate: NSObject, UIApplicationDelegate {
var didFinishLaunching: ((AppDelegate) -> Void)?
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions
launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
) -> Bool {
didFinishLaunching?(self)
return true
}
}
#main
struct MyApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#ObservedObject private var applicationModel = ApplicationModel()
// `init` gets called BEFORE `didFinishLaunchingWithOptions`
init() {
// Subscribe to get a `didFinishLaunching` call
appDelegate.didFinishLaunching = { [weak applicationObject] appDelegate in
// Setup any application code...
applicationModel?.setup()
}
}
var body: some Scene {
return WindowGroup {
if applicationObject.isUserLoggedIn {
LoggedInView()
} else {
LoggedOutView()
}
}
}
}

How to pass the presenting view controller and client ID for your app to the Google Sign In sign-in method

Hello I have been following (https://firebase.google.com/docs/auth/ios/google-signin) to setup a simple Google SignIn with Firebase in an iOS app. I use Xcode 12.5.1. I created a new Storyboard (UIKit) app. I did pod init and added Firebase and GoogleSignIn and did pod install.
I'm also total noob in iOS.
I'm trying to follow the steps and for non of the steps it really explains where to put the code. For some I think I succeeded, but for the step 4. I have no idea where to put that code. I tried looking for other posts/tutorials for setting up GoogleSignIn with Firebase and SwiftUI and non of them work. Steps seem pretty simple, but they probably omit something very simple that a noob in iOS doesn't know.
Can some1 please explain a bit this step 4 at least.
Some of you commented to put it in the AppDelegate. I have created AppDelegate, but not sure where to put it there. Creating a signIn function in AppDelegate for dumping the code from the step 4 does not work. I also tried to wrap it with UIViewControllerRepresentable like here with the same problem.
It complains that this presenting: self from GIDSignIn.sharedInstance.signIn(with: config, presenting: self) { [unowned self] user, error in (code from step 4 that I don't know where to put) is AppDelegate that can't be converted to UIViewController that is expected.
import SwiftUI
import Firebase
import GoogleSignIn
#main
struct FolderlesApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
#available(iOS 9.0, *)
func application(_ application: UIApplication, open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any])
-> Bool {
return GIDSignIn.sharedInstance.handle(url)
}
}
I think you are using SwfitUI not UIkit in the code you provided. If this is true. I can answer the SwiftUI solution.
Assume you have already set up the Step 1 (URL Types) and import the GoogleService-Info.plist. If you don’t, make sure to do it first.
Then, we have to create AppDelegate by ourselves in SwiftUI. You can add this code into your “App” type struct file (usually, the file name is your Project_NameApp.swift). Just like the example down below. This is step 2, step 3 in Authenticate Using Google Sign-In on iOS  |  Firebase
// Project_NameApp.swift
struct Project_NameApp: App {
// add this var define line
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
TestLoginView()
}
}
}
// add this class
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
// add this function for init FirebassApp
FirebaseApp.configure()
return true
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
// add this function for Google Sign in to handle the url
return GIDSignIn.sharedInstance.handle(url)
}
}
The next one is your main question - Where the step 4’s code should we be put? Following code is the example SwiftUI view that you want to login with google. You can see it just turn the step 4 code into the function, and bind to the Button you want the user to tap and Sign in with a Google account.
import SwiftUI
import GoogleSignIn
import Firebase
struct TestLoginView: View {
var body: some View {
VStack {
Button(action: {
// Just add the action function, it will works.
googleSignIn()
}, label: {
Text("Google Sign In")
})
}
}
func googleSignIn(){
guard let clientID = FirebaseApp.app()?.options.clientID else { return }
// Create Google Sign In configuration object.
let config = GIDConfiguration(clientID: clientID)
// Start the sign in flow!
GIDSignIn.sharedInstance.signIn(with: config, presenting: (UIApplication.shared.windows.first?.rootViewController)!) { user, error in
if let error = error {
print(error.localizedDescription)
return
}
guard
let authentication = user?.authentication,
let idToken = authentication.idToken
else {
return
}
let credential = GoogleAuthProvider.credential(withIDToken: idToken,
accessToken: authentication.accessToken)
// Authenticate with Firebase using the credential object
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error {
print("authentication error \(error.localizedDescription)")
return
}
print(authResult ?? "none")
}
}
}
}
struct TestLoginView_Previews: PreviewProvider {
static var previews: some View {
TestLoginView()
}
}
I have tested it on my Xcode and it works for me. You can try it and hope you can solve the problem!
Tips 1: This is just a simple code. If you want a better implementation. I recommend you implement with MVVM structure. In this case, you should put the function into your ViewModel which you create by yourself. The button should call the Google Sign in function in your ViewModel.
Tips 2: This code doesn’t implement the multi-factor login with phone verify, check out this article for more detail. Most use case won’t use this less frequently used multi-factor login.

Using Firebase Authentication with new #App protocol

I am new to iOS development and am struggling to implement Firebase phone authentication with the new #App protocol in SwiftUI. The documentation for Firebase is written for UIKit, and the tutorials online for SwiftUI use AppDelegate and SceneDelegate instead of the new #main protocol in the App struct.
My concrete questions are as follows: How/where would I inject this authentication state class I have created?
import Foundation
class AuthenticationState: NSObject, ObservableObject
{
#Published var loggedInUser: User?
#Published var isAuthenticating = false
#Published var error: NSError?
static let shared = AuthenticationState();
private let auth = Auth.auth();
fileprivate var currentNonce: String?
func login(with loginOption: LoginOption) {
self.isAuthenticating = true
self.error = nil
switch loginOption {
case .signInWithApple:
handleSignInWithApple()
}
}
func signup(email: String, password: String, passwordConfirmation: String) {
// TODO
}
private func handleSignInWithApple() {
// TODO
}
}
Secondly, the AuthenticationState class does not know about Firebase Auth object, I assume because it is incorrectly configured. So far, I am configuring Firebase in an AppDelegate class:
import UIKit
import Firebase
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
return true
}
}
Whereas, I also have a MapworkApp.swift file, which I believe is supposed to replace AppDelegate, but I am unsure:
import SwiftUI
import Firebase
#main
struct MapworkApp: App {
let persistenceController = PersistenceController.shared
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
struct MapworkApp_Previews: PreviewProvider {
static var previews: some View {
/*#START_MENU_TOKEN#*/Text("Hello, World!")/*#END_MENU_TOKEN#*/
}
}
The runtime errors I currently receive:
020-12-16 13:22:34.416917-0700 Mapwork[1351:332863] 7.3.0 - [Firebase/Core][I-COR000003] The default Firebase app has not yet been configured. Add `[FIRApp configure];` (`FirebaseApp.configure()` in Swift) to your application initialization. Read more:
2020-12-16 13:22:34.417240-0700 Mapwork[1351:332633] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'The default FIRApp instance must be configured before the default FIRAuthinstance can be initialized. One way to ensure that is to call `[FIRApp configure];` (`FirebaseApp.configure()` in Swift) in the App Delegate's `application:didFinishLaunchingWithOptions:` (`application(_:didFinishLaunchingWithOptions:)` in Swift).'
*** First throw call stack:
(0x1863a586c 0x19b314c50 0x18629e4a4 0x102bc8918 0x1028465d8 0x1028466f4 0x102845c1c 0x102845be4 0x102ec96c0 0x102ecb1f8 0x18a32c5bc 0x102845c64 0x102854aa0 0x102854cbc 0x18cdba724 0x102854c18 0x102855028 0x185fda6b0)
libc++abi.dylib: terminating with uncaught exception of type NSException
terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'The default FIRApp instance must be configured before the default FIRAuthinstance can be initialized. One way to ensure that is to call `[FIRApp configure];` (`FirebaseApp.configure()` in Swift) in the App Delegate's `application:didFinishLaunchingWithOptions:` (`application(_:didFinishLaunchingWithOptions:)` in Swift).'
Message from debugger: The LLDB RPC server has exited unexpectedly. Please file a bug if you have reproducible steps.
Any help or direction would be greatly appreciated as the docs online are now inapplicable.
For many Firebase APIs, you don't need to use the AppDelegate approach. Instead, you can get away with initialising Firebase in your app's initialiser, like so:
import SwiftUI
import Firebase
#main
struct MapworkApp: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
This works well for apps that use Cloud Firestore and Firebase Authentication (Sign in with Apple).
For some of Firebase's SDKs you need to implement an AppDelegate. To do so, implement your AppDelegate like this:
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("Application is starting up. ApplicationDelegate didFinishLaunchingWithOptions.")
return true
}
// ... add any callbacks your app might require
}
Then, connect your App to the AppDelegate like this:
#main
struct MapworkApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
You might have to move the call to FirebaseApp.configure() to the AppDelegate. In this case, your AppDelegate and App might look like this (you can keep them in the same file, if you like):
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
return true
}
}
#main
struct MapworkApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
To learn more about this, check out my articles about the new application life cycle:
Firebase and the new SwiftUI 2 Application Life Cycle - SwiftUI 2
The Ultimate Guide to the SwiftUI 2 Application Life Cycle - SwiftUI 2
This sample app I wrote shows how to build a simple to-do list app with SwiftUI and Firebase: peterfriese/MakeItSo. The latest version of the code follows the new SwiftUI 2 Application Life Cycle, and includes Sign in with Apple.

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