137

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
}
Paulo Mattos
  • 18,845
  • 10
  • 77
  • 85
M1X
  • 4,971
  • 10
  • 61
  • 123

7 Answers7

128

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()
        }
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Awesome, Any idea how to update existing app to use SwiftUI life-cycle? – M1X Jun 23 '20 at 15:39
  • 5
    @RexhinHoxha, set deployment target to iOS 14, remove `@UIApplicationMain` from AppDelegate, and add is-a `App` struct like above. – Asperi Jun 23 '20 at 15:49
  • Sorry, I know this question is a bit old but I'm trying to utilize it to change the Navigation Bar background. Can this be done with SwiftUI with putting the normal `UINavigationBar.appearance().barTintColor = UIColor.red` where it says `>> your code here !!` – Jason Brady Jan 15 '21 at 15:49
  • Based on your answer, I added the notification related functions into the AppDelegate class. In my case, when I call _ContentView().environmentObject(Client())_, how do I pass the app token from the App delegate to my Client object? Should that be the initialization variable? – Nazar Jun 26 '22 at 21:20
  • It gives me an error in macOS "Cannot find type 'UIApplicationDelegate' in scope" – Bijender Singh Shekhawat Aug 30 '22 at 11:27
94

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:

Peter Friese
  • 6,709
  • 31
  • 43
  • 1
    nice solution, what about if we have smth like this? ` func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool { GIDSignIn.sharedInstance().handle(url) }` – M1X Jun 25 '20 at 11:25
  • 1
    @RexhinHoxha I think there is a new `onOpenURL` method that you can use to do exactly that (I remember seeing one in a session, however, I didn't get it working in b1 yet). – Tom Shen Jun 27 '20 at 10:41
  • I've put together a more thorough write-up that shows how to initialise Firebase in SwiftUI 2 apps: https://peterfriese.dev/swiftui-new-app-lifecycle-firebase/ – Peter Friese Oct 19 '20 at 21:40
  • 4
    I've noticed that when using this I get a warning on the console: [GoogleUtilities/AppDelegateSwizzler][I-SWZ001014] App Delegate does not conform to UIApplicationDelegate protocol. – bze12 Dec 24 '20 at 02:09
  • See my answer here https://stackoverflow.com/a/65420572/281221 for details about Phone Auth, @bze12. – Peter Friese Dec 30 '20 at 14:48
  • 1
    `init()` gets called BEFORE `applicationDidFinishLaunching`. This does not work for `Firebase.configure()`, Firebase throws exception if you put `configure()` in init() of App. – kgaidis May 17 '21 at 23:55
  • @kgaidis I've been calling FirebaseApp.configure() inside App.init() in several of my apps, and didn't run into any issues. You are right in that App.init() gets called before didFinishLaunchingWithOptions - so you need to take care when you use *both* an AppDelegate and the App initialiser. Check out my article to see a detailed explanation: https://peterfriese.dev/swiftui-new-app-lifecycle-firebase/ – Peter Friese May 18 '21 at 13:22
  • @PeterFriese I see that the Firebase website has updated the "add this code" page when downloading the GoogleService-Info.plist file, but they reference the `@UIApplicationDelegateAdaptor` method and the console shows an error message when using `init()`. Do you know why they prefer/recommend the other way? – Zonker.in.Geneva Jun 26 '22 at 22:32
  • 1
    Hi @Zonker.in.Geneva - I was involved in making this update to our setup flow. We decided to use the AppDelegateAdaptor approach, as it also works for FCM and phone auth. The simplified setup works well for other services, such as Cloud Firestore, Cloud Storage, and most Auth providers. – Peter Friese Jun 28 '22 at 05:11
  • 1
    Please keep in mind, that overriding the init can lead to very weird behavior. One thing is for example the app accent color, this is reset by this init. – Tob Aug 07 '22 at 15:42
  • @Tob - can you elaborate? – Peter Friese Aug 07 '22 at 16:28
  • @PeterFriese sorry, I disabled all the notifications. I have a green tint color for example. Overriding the init of the app causes some values to fallback on their defaults. Tint color was blue again. – Tob Sep 04 '22 at 09:57
46

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.

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • 1
    what about this piece of code? where do I put it? `func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool { GIDSignIn.sharedInstance().handle(url) }` – M1X Jul 13 '20 at 16:47
  • I have added more details about that to my answer. – Mojtaba Hosseini Jul 13 '20 at 18:55
  • Looks far more complex than it needs to be. Better off just waiting for Google to fix their swizzling issue, which they are working on. – Peter Suwara Aug 26 '20 at 17:08
  • How to generate a Combine Publisher for didReceiveRemoteNotification? Could you please answer my question at https://stackoverflow.com/questions/64512868/how-to-generate-a-combine-publisher-for-didreceiveremotenotification – Mohamed Wasiq Oct 24 '20 at 11:33
  • "killing default multiplatform support" which platforms you mean? – David Chelidze Jan 27 '21 at 10:58
  • Great answer above. Works for me. Using Xcode 14.1 for iOS 16.1 – LizG Dec 02 '22 at 05:50
22

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/

MScottWaller
  • 3,321
  • 2
  • 24
  • 47
  • I get `'Failed to get FirebaseApp instance. Please call FirebaseApp.configure() before using Firestore' terminating with uncaught exception of type NSException` when I put `FirebaseApp.configure()` in case .active: – M1X Jun 28 '20 at 00:31
  • You might try configuring Firebase by using the ```init() { FirebaseApp.configure() } ``` As show in one of the other answers – MScottWaller Jun 28 '20 at 00:55
4

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()
      }
    }
  }
}
kgaidis
  • 14,259
  • 4
  • 79
  • 93
  • In the `App`'s `init`, the application state is `active`, so I would assume it's safe to do initialization stuff. – D6mi Mar 23 '22 at 16:05
2

To use the SwiftUI LifeCycle then you need to use UIApplicationDelegateAdaptor to inject an instance of a class that conforms to UIApplicationDelegate into your SwiftUI App.

  1. Create an AppDelegate class that conforms to UIApplicationDelegate
  2. In @main struct use the UIApplicationDelegateAdaptor
// Class that conforms to UIApplicationDelegate
class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        // perform what you want here
        return true
    }
}

@main
struct MyApp: App {

    // Use UIApplicationDelegateAdaptor to inject an instance of the AppDelegate
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Update for Xcode 13

In Xcode 13 you cannot selected the LifeCycle anymore, it is inferred from the Interface that you select.

enter image description here


Xcode 12 Beta

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.

enter image description here

Make sure you choose UIKit App Delegate for the Life Cycle and you will get an AppDelegate and a SceneDelegate

Andrew
  • 26,706
  • 9
  • 85
  • 101
  • 2
    So the only way to use Firebase for the moment is to choose `UIKit App Delegate` right? – M1X Jun 23 '20 at 15:26
  • Where is the Life Cycle option for an existing app? – lmh Jul 12 '20 at 23:37
  • @Imh Life Cycle is an option available when you first create an app. If you have chosen the SwiftUI App Life Cycle then you need to delete the `@main` from your `.swift` and you would need to recreate the `AppDelegate` and `SceneDelegate`. It would probably be easier creating a new project and just copying your code across. – Andrew Jul 13 '20 at 06:07
  • Unfortunately, this option is not available as of Xcode 12 Beta 6–any hints of how to go about then? I guess creating the project in Xcode 11 would still be a thing, but there must be a better solution to this... – appfrosch Sep 10 '20 at 07:08
  • 1
    It is available in Xcode 12-beta6. I’ve just checked and it exists for me. – Andrew Sep 10 '20 at 07:13
  • Yeah, you're right–just realised my mistake: when choosing cross platform, it's not there, which kind of makes sense... When choosing iOS it's there indeed. – appfrosch Sep 10 '20 at 07:20
1

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.

D6mi
  • 611
  • 7
  • 16
  • This is actually pretty nice, except for the fact, that apple, at least at the moment, does something different with a user defined init. For me accentColor was gone. – Tob Aug 07 '22 at 15:46