firebase / firebase/firebase-ios-sdk

SwiftUI and Objective-C inter-op with Swizzling

Open
#10,417 7 comments 6 reactions 1 assignee Claimed by @morganchen12 View on GitHub
SwiftUI
Dominant language
C++
Stars
6.7k
Forks
1.8k
Avg merge
2d 14h
Merged PRs (30d)
72

Description

### 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:
```swift
@main
struct FirebaseShowcaseApp: App {

init() {
FirebaseApp.configure()
}

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

Concrete **issue** example from `FIRAuth.protectedDataInitialization`:
```Objective-C
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:
```swift
@UIApplicationDelegateAdaptor(MyAppDelegate.self) var delegate
```
The wrapper class is: `SwiftUI.AppDelegate`. which looks like:
```

- super: UIResponder
- super: NSObject
▿ fallbackDelegate: Optional()
- 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:**
1. create `SwiftUI` application;
2. add Firebase;
3. create `@UIApplicationDelegateAdaptor(AppDelegate)`;
4. 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\)appDelegate just before

```shell
objc_setAssociatedObject(appDelegate, &kGULRealIMPBySelectorKey,
[realImplementationsBySelector copy], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
```

```shell
(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

```json

{
"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
}
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.