swiftuifirebase-authentication

@Observable pattern with FirebaseAuth in SwiftUI


I want to transition my FireBase Auth class to the new (iOS17) @Observable pattern. The old pattern (ObservableObject/ EnviromentObject) works. I get this error message when using the pattern. I appreciate any tips.

Thread 1: "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)."

old:

class Authentication : ObservableObject { ... } 

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_: UIApplication,
                     didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool
    {
        FirebaseApp.configure()
        return true
    }
}

@main
struct RApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
    @StateObject var authentication = Authentication()

    var body: some Scene {
        WindowGroup {
            RootView()
                .environmentObject(authentication)
                .preferredColorScheme(.dark)
        }
    }
}

new:

@Observable
class Authentication { ... }

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_: UIApplication,
                     didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool
    {
        FirebaseApp.configure()
        return true
    }
}

@main
struct RApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
    
    @State private var authentication: Authentication = Authentication()

    var body: some Scene {
        WindowGroup {
            RootView()
                .environment(authentication)
                .preferredColorScheme(.dark)
        }
    }
}

Solution

  • This is because @State runs the initialiser = Authentication() immediately, when the RApp struct initialised. This happens before didFinishLaunchingWithOptions. Presumably you are calling things like Auth.auth() in the initialiser, which should be done after configure().

    Compare this to @StateObject, which runs the initialiser a bit later, by wrapping it in a @autoclosure.

    See also @Observable vs ObservableObject

    If you want to use @Observable, you can put the @State in a View instead of the RApp struct. This makes the Authentication initialiser run a bit later.

    @main
    struct RApp: App {
        @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
    
        var body: some Scene {
            WindowGroup {
                Wrapper()
            }
        }
    }
    
    struct Wrapper: View {
        @State var authentication = Authentication()
    
        var body: some View { 
            RootView() 
                .environment(authentication)
                .preferredColorScheme(.dark)
        }
    }
    

    Alternatively, you can try restructuring your code so that you don't call Auth.auth() (or anything that cannot be called before configure()) in Authentication.init.

    For example, if you have

    @Observable
    class Authentication {
        let auth = Auth.auth()
    
        // ...
    }
    

    You can change this to

    @Observable
    class Authentication {
        @ObservationIgnored
        lazy var auth = Auth.auth()
    
        // ...
    }
    

    and make sure you don't access auth in init.

    In any case though, I think you should just keep using ObservableObject. It's not deprecated or anything. There is no need to migrate to @Observable if it just makes your life harder.