0

I am writing an app in SwiftUI and using Firebase as my backend.

I have a working preferences panel, that consists of a modal within the application itself, but am wanting to utilise macOS's consistent preferences window that includes the menu options as well as shortcut that is written about in this article.

This is my App declaration and delegates.

@main
struct SchedulerApp: App {
    #if os(macOS)
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    #else
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    #endif
    
    @StateObject var authState = AuthState()
    @StateObject var model = Model()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .frame(minWidth: 800, minHeight: 450)
                .environmentObject(model)
                .environmentObject(authState)
            
        }
        .commands {
            SidebarCommands()
        }
        
        #if os(macOS)
        Settings {
            Preferences()
        }
        #endif
    }
}

#if os(macOS)
class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationWillFinishLaunching(_ notification: Notification) {
        FirebaseApp.configure()
    }
}
#else
class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        FirebaseApp.configure()
        
        
        
        return true
    }
}
#endif

You can see that I have both a delegate and a state object for an object that listens to the authentication state and another that holds user data and settings:

class AuthState: ObservableObject {
    var handle: AuthStateDidChangeListenerHandle?
    
    @Published var signedIn: Bool? = nil
    
    init() {
        listen()
    }
    
    func listen() {
        Auth.auth().addStateDidChangeListener { [self] (auth, user) in
            switch user == nil {
            case true: signedIn = false
            case false: signedIn = true
            }
        }
    }
}

However, when I run this code, the following error occurs:

The default Firebase app has not yet been configured. Add [FIRApp configure]; (FirebaseApp.configure() in Swift) to your application initialization. Read more: (...)

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).

I need the app to have a singular source of truth at the initialisation step - but the applicationWillFinishLaunching for some reason is calling after the StateObject call.

My app currently has these StateObject classes declared in ContentView, which dodges these errors, but as I mentioned above, I need preferences to share a single source of truth.

Any help is much appreciated!

Tahmid Azam
  • 566
  • 4
  • 16

2 Answers2

2

Within SchedulerApp: App just add an init to initialize Firebase. You would no longer need the AppDelegate class (unless your going to do other stuff in there later).

 init() { 
    FirebaseApp.configure()
 }
nicksarno
  • 3,850
  • 1
  • 13
  • 33
1

As discussed in Where to configure Firebase in my iOS app in the new SwiftUI App life cycle without AppDelegate and SceneDelegate?, you can either use your app's initialiser (init()) or an AppDelegate to initialise Firebase.

Using the initialiser works for quite a few of Firebase's SDKs, but there are some that make use of method swizzling, and they expect the presence of an AppDelegate.

In The Ultimate Guide to the SwiftUI 2 Application Life Cycle - SwiftUI 2, I dive into some of the details of the new app life cycle.

Let's take a look at the code for the sample app I built for that article, and the resulting output:

class AppDelegate: NSObject, UIApplicationDelegate {
  func application(_ application: UIApplication,
                   didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    print("Colors application is starting up. ApplicationDelegate didFinishLaunchingWithOptions.")
    return true
  }
}


@main
struct ColorsApp: App {
  @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
  
  @Environment(\.scenePhase) var scenePhase
  
  init() {
    print("Colors application is starting up. App initialiser.")
  }
  
  var body: some Scene {
    WindowGroup {
      ContentView()
    }
    .onChange(of: scenePhase) { newScenePhase in
      switch newScenePhase {
      case .active:
        print("App is active")
      case .inactive:
        print("App is inactive")
      case .background:
        print("App is in background")
      @unknown default:
        print("Oh - interesting: I received an unexpected new value.")
      }
    }
  }
}

Debug console output:

Colors application is starting up. App initialiser.
Colors application is starting up. ApplicationDelegate didFinishLaunchingWithOptions.
App is active

So the app initialisation happens as follows:

  1. The app initialiser is called
  2. The AppDelegate's didFinishLaunchingWithOptions is called
  3. The scenePhase changes to .active

For your app, I'd recommend the following:

  1. Initialise Firebase in your app's initialiser
  2. In your app's initialiser, initialise your models and other sources of truth, and connect them to the respective Firebase services

Another option is:

  1. Initialise Firebase in your app's initialiser
  2. Initialise your models and sources of truths by providing default values (as you did), but don't connect to Firebase just yet
  3. Implement a ScenePhase handler and - once the app goes into .active state, "activate" your models and tell them to connect to Firebase.

The first option sounds a lot cleaner and easier to implement. to me :-)

Peter Friese
  • 6,709
  • 31
  • 43