SwiftUI app life cycle iOS14 where to put AppDelegate code?
Asked Answered
I

7

147

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
}
India answered 23/6, 2020 at 15:14 Comment(0)
S
143

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()
        }
    }
}
Shinar answered 23/6, 2020 at 15:29 Comment(5)
Awesome, Any idea how to update existing app to use SwiftUI life-cycle?India
@RexhinHoxha, set deployment target to iOS 14, remove @UIApplicationMain from AppDelegate, and add is-a App struct like above.Shinar
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 !!Maltreat
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?Sphacelus
It gives me an error in macOS "Cannot find type 'UIApplicationDelegate' in scope"Downfall
K
97

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:

Kilian answered 24/6, 2020 at 19:45 Comment(12)
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) }`India
@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).Scandura
I've put together a more thorough write-up that shows how to initialise Firebase in SwiftUI 2 apps: peterfriese.dev/swiftui-new-app-lifecycle-firebaseKilian
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.Barbur
See my answer here https://mcmap.net/q/160754/-swiftui-swizzling-disabled-by-default-phone-auth-not-working for details about Phone Auth, @bze12.Kilian
init() gets called BEFORE applicationDidFinishLaunching. This does not work for Firebase.configure(), Firebase throws exception if you put configure() in init() of App.Verbenaceous
@Verbenaceous 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: peterfriese.dev/swiftui-new-app-lifecycle-firebaseKilian
@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?Malave
Hi @Malave - 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.Kilian
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.Calefacient
@Calefacient - can you elaborate?Kilian
@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.Calefacient
T
47

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.

Tactician answered 13/7, 2020 at 16:17 Comment(6)
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) }India
I have added more details about that to my answer.Tactician
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.Brad
How to generate a Combine Publisher for didReceiveRemoteNotification? Could you please answer my question at #64513368Ratable
"killing default multiplatform support" which platforms you mean?Escalator
Great answer above. Works for me. Using Xcode 14.1 for iOS 16.1Cocci
S
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/

Sevier answered 27/6, 2020 at 23:55 Comment(2)
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:India
You might try configuring Firebase by using the init() { FirebaseApp.configure() } As show in one of the other answersSevier
V
6

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()
      }
    }
  }
}
Verbenaceous answered 18/5, 2021 at 0:19 Comment(1)
In the App's init, the application state is active, so I would assume it's safe to do initialization stuff.Boice
B
5

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

Bootstrap answered 23/6, 2020 at 15:24 Comment(6)
So the only way to use Firebase for the moment is to choose UIKit App Delegate right?India
Where is the Life Cycle option for an existing app?Wiring
@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 <ProjectName>.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.Bootstrap
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...Panhandle
It is available in Xcode 12-beta6. I’ve just checked and it exists for me.Bootstrap
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.Panhandle
B
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.

Boice answered 23/3, 2022 at 16:14 Comment(1)
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.Calefacient

© 2022 - 2024 — McMap. All rights reserved.