Description
First of all, let's define some terminology. I will refer to:
- a "SwiftUI app" an app that declares its entry point as being the implementation of the
SwiftUI.App protocol, it being marked by the new @main annotation; This app can still have AppDelegate and SceneDelegate implementations, but those will not be the entry point of the application;
Using FirebaseApp in a "SwiftUI app" is very confusing and there is no guidance as to where to correctly place the FirebaseApp.configure() method call and more importantly, why place it where it should be placed.
Some of my observations from placing the FirebaseApp.configure() call in different places:
Configuration
FirebaseApp.configure() in the App's init method
Works, but some of the components from Firebase are reliant on the UIApplication.shared instance, which is not initialized at the time of the call; ❌
Setup:
@main
struct FirebaseShowcaseApp: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Concrete issue example from FIRAuth.protectedDataInitialization:
UIApplication *application = [applicationClass sharedApplication];
if (application) {
// Initialize for phone number auth.
strongSelf->_tokenManager = [[FIRAuthAPNSTokenManager alloc] initWithApplication:application];
strongSelf->_appCredentialManager =
[[FIRAuthAppCredentialManager alloc] initWithKeychain:strongSelf->_keychainServices];
strongSelf->_notificationManager = [[FIRAuthNotificationManager alloc]
initWithApplication:application
appCredentialManager:strongSelf->_appCredentialManager];
}
at this time and point, as far as I could investigate and test, the application is NULL and that doesn't seem to change during the lifetime of the app in this setup.
Obs: swizzling on/ off does not solve the behaviour described.
FirebaseApp.configure() in UIApplicationDelegate.application(_, didFinishLaunchingWithOptions:)
Solves the problem described above since at init time the UIApplication.shared exists. But this is where the discussion gets interesting.
- with swizzling disabled everything works as expected; ✅
- with swizzling enabled, the
UIApplicationDelegate methods do not get correctly called; ❌ the investigation that led me to this conclusion is the tap DynamicLink -> app not installed -> install app -> navigate to the actual deeplink scenario. I was simply not getting the UIApplicationDelegate.application(_:open:options:) or any other method, for what is worth, called. A couple of hours later and down the rabbit hole I have discovered that SwiftUI creates its own AppDelegate wrapper over the actual MyAppDelegate declared in my application like:
@UIApplicationDelegateAdaptor(MyAppDelegate.self) var delegate
The wrapper class is: SwiftUI.AppDelegate. which looks like:
<SwiftUI.AppDelegate: 0x2839f6520>
- super: UIResponder
- super: NSObject
▿ fallbackDelegate: Optional(<MyApp.MyAppDelegate: 0x2822cdac0>)
- mainMenuController: nil
▿ appNavigationAuthority: SwiftUI.AppNavigationAuthority
The issue with this wrapper class is that it's not fully interoperable with Objective-C runtime causing the swizzling to fail.
Reproducing the issue
Obs: does not seem to be an iOS specific issue since it doesn't work on iOS 15 or iOS 16 either.
Reproduction steps:
- create
SwiftUI application;
- add Firebase;
- create
@UIApplicationDelegateAdaptor(AppDelegate);
- call
FirebaseApp.configure() from UIApplicationDelegate.application(_, didFinishLaunchingWithOptions:);
Firebase SDK Version
9.6.0
Xcode Version
14.0.1
Installation Method
Swift Package Manager
Firebase Product(s)
Analytics, Authentication, Database, DynamicLinks, Firestore, Functions, Remote Config
Targeted Platforms
iOS
Relevant Log Output
Setup: FirebaseApp.configure() called in from a SwiftUI app from UIApplicationDelegate.application(_, didFinishLaunchingWithOptions:).
From: GULAppDelegateSwizzler.createSubclassWithObject:(id<GULApplicationDelegate>)appDelegate just before
objc_setAssociatedObject(appDelegate, &kGULRealIMPBySelectorKey,
[realImplementationsBySelector copy], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
(lldb) po realImplementationsBySelector
{
"application:continueUserActivity:restorationHandler:" = "{length = 8, bytes = 0x0000000000000000}";
"application:handleEventsForBackgroundURLSession:completionHandler:" = "{length = 8, bytes = 0x0000000000000000}";
"application:openURL:options:" = "{length = 8, bytes = 0x0000000000000000}";
"application:openURL:sourceApplication:annotation:" = "{length = 8, bytes = 0x0000000000000000}";
}
If using Swift Package Manager, the project's Package.resolved
Expand Package.resolved snippet
{
"pins" : [
{
"identity" : "abseil-cpp-swiftpm",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/abseil-cpp-SwiftPM.git",
"state" : {
"revision" : "583de9bd60f66b40e78d08599cc92036c2e7e4e1",
"version" : "0.20220203.2"
}
},
{
"identity" : "appauth-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/openid/AppAuth-iOS.git",
"state" : {
"revision" : "3d36a58a2b736f7bc499453e996a704929b25080",
"version" : "1.6.0"
}
},
{
"identity" : "boringssl-swiftpm",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/boringssl-SwiftPM.git",
"state" : {
"revision" : "dd3eda2b05a3f459fc3073695ad1b28659066eab",
"version" : "0.9.1"
}
},
{
"identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk.git",
"state" : {
"revision" : "7e80c25b51c2ffa238879b07fbfc5baa54bb3050",
"version" : "9.6.0"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "c1cfde8067668027b23a42c29d11c246152fe046",
"version" : "9.6.0"
}
},
{
"identity" : "googledatatransport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleDataTransport.git",
"state" : {
"revision" : "5056b15c5acbb90cd214fe4d6138bdf5a740e5a8",
"version" : "9.2.0"
}
},
{
"identity" : "googlesignin-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleSignIn-iOS",
"state" : {
"revision" : "9450e779619fc184d360c9f7ce61023587f7e1f4",
"version" : "6.2.2"
}
},
{
"identity" : "googleutilities",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "22907832079d808e82f1182b21af58ef3880666f",
"version" : "7.8.0"
}
},
{
"identity" : "grpc-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/grpc/grpc-ios.git",
"state" : {
"revision" : "8440b914756e0d26d4f4d054a1c1581daedfc5b6",
"version" : "1.44.3-grpc"
}
},
{
"identity" : "gtm-session-fetcher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/gtm-session-fetcher.git",
"state" : {
"revision" : "4e9bbf2808b8fee444e84a48f5f3c12641987d3e",
"version" : "1.7.2"
}
},
{
"identity" : "gtmappauth",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GTMAppAuth.git",
"state" : {
"revision" : "6dee0cde8a1b223737a5159e55e6b4ec16bbbdd9",
"version" : "1.3.1"
}
},
{
"identity" : "leveldb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/leveldb.git",
"state" : {
"revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b",
"version" : "1.22.2"
}
},
{
"identity" : "nanopb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/nanopb.git",
"state" : {
"revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692",
"version" : "2.30909.0"
}
},
{
"identity" : "promises",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/promises.git",
"state" : {
"revision" : "3e4e743631e86c8c70dbc6efdc7beaa6e90fd3bb",
"version" : "2.1.1"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "b8230909dedc640294d7324d37f4c91ad3dcf177",
"version" : "1.20.1"
}
}
],
"version" : 2
}
Description
First of all, let's define some terminology. I will refer to:
SwiftUI.Appprotocol, it being marked by the new@mainannotation; This app can still haveAppDelegateandSceneDelegateimplementations, but those will not be the entry point of the application;Using
FirebaseAppin a "SwiftUI app" is very confusing and there is no guidance as to where to correctly place theFirebaseApp.configure()method call and more importantly, why place it where it should be placed.Some of my observations from placing the
FirebaseApp.configure()call in different places:Configuration
FirebaseApp.configure()in theApp'sinitmethodWorks, but some of the components from
Firebaseare reliant on theUIApplication.sharedinstance, which is not initialized at the time of the call; ❌Setup:
Concrete issue example from
FIRAuth.protectedDataInitialization:at this time and point, as far as I could investigate and test, the
application is NULLand that doesn't seem to change during the lifetime of the app in this setup.Obs: swizzling on/ off does not solve the behaviour described.
FirebaseApp.configure()inUIApplicationDelegate.application(_, didFinishLaunchingWithOptions:)Solves the problem described above since at init time the
UIApplication.sharedexists. But this is where the discussion gets interesting.UIApplicationDelegatemethods do not get correctly called; ❌ the investigation that led me to this conclusion is thetap DynamicLink -> app not installed -> install app -> navigate to the actual deeplinkscenario. I was simply not getting theUIApplicationDelegate.application(_:open:options:)or any other method, for what is worth, called. A couple of hours later and down the rabbit hole I have discovered thatSwiftUIcreates its ownAppDelegatewrapper over the actualMyAppDelegatedeclared in my application like:The wrapper class is:
SwiftUI.AppDelegate. which looks like:The issue with this wrapper class is that it's not fully interoperable with
Objective-C runtimecausing theswizzlingto fail.Reproducing the issue
Obs: does not seem to be an iOS specific issue since it doesn't work on iOS 15 or iOS 16 either.
Reproduction steps:
SwiftUIapplication;@UIApplicationDelegateAdaptor(AppDelegate);FirebaseApp.configure()fromUIApplicationDelegate.application(_, didFinishLaunchingWithOptions:);Firebase SDK Version
9.6.0
Xcode Version
14.0.1
Installation Method
Swift Package Manager
Firebase Product(s)
Analytics, Authentication, Database, DynamicLinks, Firestore, Functions, Remote Config
Targeted Platforms
iOS
Relevant Log Output
Setup:
FirebaseApp.configure()called in from aSwiftUI appfromUIApplicationDelegate.application(_, didFinishLaunchingWithOptions:).From: GULAppDelegateSwizzler.createSubclassWithObject:(id<GULApplicationDelegate>)appDelegate just before
(lldb) po realImplementationsBySelector { "application:continueUserActivity:restorationHandler:" = "{length = 8, bytes = 0x0000000000000000}"; "application:handleEventsForBackgroundURLSession:completionHandler:" = "{length = 8, bytes = 0x0000000000000000}"; "application:openURL:options:" = "{length = 8, bytes = 0x0000000000000000}"; "application:openURL:sourceApplication:annotation:" = "{length = 8, bytes = 0x0000000000000000}"; }If using Swift Package Manager, the project's Package.resolved
Expand
Package.resolvedsnippet{ "pins" : [ { "identity" : "abseil-cpp-swiftpm", "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/abseil-cpp-SwiftPM.git", "state" : { "revision" : "583de9bd60f66b40e78d08599cc92036c2e7e4e1", "version" : "0.20220203.2" } }, { "identity" : "appauth-ios", "kind" : "remoteSourceControl", "location" : "https://github.com/openid/AppAuth-iOS.git", "state" : { "revision" : "3d36a58a2b736f7bc499453e996a704929b25080", "version" : "1.6.0" } }, { "identity" : "boringssl-swiftpm", "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/boringssl-SwiftPM.git", "state" : { "revision" : "dd3eda2b05a3f459fc3073695ad1b28659066eab", "version" : "0.9.1" } }, { "identity" : "firebase-ios-sdk", "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk.git", "state" : { "revision" : "7e80c25b51c2ffa238879b07fbfc5baa54bb3050", "version" : "9.6.0" } }, { "identity" : "googleappmeasurement", "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { "revision" : "c1cfde8067668027b23a42c29d11c246152fe046", "version" : "9.6.0" } }, { "identity" : "googledatatransport", "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleDataTransport.git", "state" : { "revision" : "5056b15c5acbb90cd214fe4d6138bdf5a740e5a8", "version" : "9.2.0" } }, { "identity" : "googlesignin-ios", "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleSignIn-iOS", "state" : { "revision" : "9450e779619fc184d360c9f7ce61023587f7e1f4", "version" : "6.2.2" } }, { "identity" : "googleutilities", "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleUtilities.git", "state" : { "revision" : "22907832079d808e82f1182b21af58ef3880666f", "version" : "7.8.0" } }, { "identity" : "grpc-ios", "kind" : "remoteSourceControl", "location" : "https://github.com/grpc/grpc-ios.git", "state" : { "revision" : "8440b914756e0d26d4f4d054a1c1581daedfc5b6", "version" : "1.44.3-grpc" } }, { "identity" : "gtm-session-fetcher", "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { "revision" : "4e9bbf2808b8fee444e84a48f5f3c12641987d3e", "version" : "1.7.2" } }, { "identity" : "gtmappauth", "kind" : "remoteSourceControl", "location" : "https://github.com/google/GTMAppAuth.git", "state" : { "revision" : "6dee0cde8a1b223737a5159e55e6b4ec16bbbdd9", "version" : "1.3.1" } }, { "identity" : "leveldb", "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/leveldb.git", "state" : { "revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b", "version" : "1.22.2" } }, { "identity" : "nanopb", "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/nanopb.git", "state" : { "revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692", "version" : "2.30909.0" } }, { "identity" : "promises", "kind" : "remoteSourceControl", "location" : "https://github.com/google/promises.git", "state" : { "revision" : "3e4e743631e86c8c70dbc6efdc7beaa6e90fd3bb", "version" : "2.1.1" } }, { "identity" : "swift-protobuf", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { "revision" : "b8230909dedc640294d7324d37f4c91ad3dcf177", "version" : "1.20.1" } } ], "version" : 2 }