Skip to content

Commit cfed377

Browse files
Feature: Allow specifying AppDelegate via Info.plist for RunOnAppDelegateClasses
Currently, `firebase::util::RunOnAppDelegateClasses` on iOS automatically swizzles `[UIApplication setDelegate:]` to capture and act on any class set as the application delegate. This change introduces an optional feature where developers can specify their app's main AppDelegate class name directly in the `Info.plist` file using the key `FirebaseAppDelegateClassName`. If this key is present and provides a valid class name: - `RunOnAppDelegateClasses` will only execute blocks for this specified class. - `[UIApplication setDelegate:]` will NOT be swizzled by Firebase. If the key is not present, is invalid, or the specified class is not found, Firebase will fall back to the original behavior of swizzling `[UIApplication setDelegate:]`. This provides developers more control over Firebase's interaction with the AppDelegate, especially in scenarios where swizzling might be undesirable or needs to be more targeted. Detailed logging has been added to trace the behavior in both modes. A manual testing plan has been outlined to cover various scenarios.
1 parent 889ab61 commit cfed377

File tree

1 file changed

+108
-22
lines changed

1 file changed

+108
-22
lines changed

app/src/util_ios.mm

Lines changed: 108 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@
2727
#import <UIKit/UIKit.h>
2828
#import <objc/runtime.h>
2929

30+
// Key for Info.plist to specify the AppDelegate class name.
31+
static NSString *const kFirebaseAppDelegateClassNameKey = @"FirebaseAppDelegateClassName";
32+
33+
// Flag to indicate if Firebase should only run on a specific AppDelegate class
34+
// specified via Info.plist, bypassing setDelegate: swizzling.
35+
static bool g_firebase_specific_delegate_mode = false;
36+
// Stores the specific AppDelegate class if specified via Info.plist.
37+
static Class _Nullable g_firebase_target_app_delegate_class = nil;
38+
3039
#define MAX_PENDING_APP_DELEGATE_BLOCKS 8
3140
#define MAX_SEEN_DELEGATE_CLASSES 32
3241

@@ -163,6 +172,38 @@ @implementation UIApplication (FirebaseAppDelegateSwizzling)
163172
+ (void)load {
164173
static dispatch_once_t onceToken;
165174
dispatch_once(&onceToken, ^{
175+
// Check Info.plist for the specific AppDelegate class name
176+
NSString *appDelegateClassName = [[NSBundle mainBundle] objectForInfoDictionaryKey:kFirebaseAppDelegateClassNameKey];
177+
178+
if (appDelegateClassName && [appDelegateClassName isKindOfClass:[NSString class]] && appDelegateClassName.length > 0) {
179+
Class specificAppDelegateClass = NSClassFromString(appDelegateClassName);
180+
if (specificAppDelegateClass) {
181+
NSLog(@"Firebase: Info.plist key '%@' found. Targeting AppDelegate class: %@. Swizzling of setDelegate: will be skipped.", kFirebaseAppDelegateClassNameKey, appDelegateClassName);
182+
g_firebase_specific_delegate_mode = true;
183+
g_firebase_target_app_delegate_class = specificAppDelegateClass;
184+
185+
// Clear any previously seen classes and set only the target one.
186+
// This makes g_seen_delegate_classes consistent for RunOnAppDelegateClasses.
187+
for (int i = 0; i < g_seen_delegate_classes_count; i++) {
188+
g_seen_delegate_classes[i] = nil;
189+
}
190+
g_seen_delegate_classes[0] = specificAppDelegateClass;
191+
g_seen_delegate_classes_count = 1;
192+
193+
NSLog(@"Firebase: %@ is now the only 'seen' delegate class.", appDelegateClassName);
194+
return; // IMPORTANT: Do not proceed to swizzle setDelegate:
195+
} else {
196+
NSLog(@"Firebase Error: Info.plist key '%@' provided class name '%@', but this class was not found. Proceeding with default setDelegate: swizzling.", kFirebaseAppDelegateClassNameKey, appDelegateClassName);
197+
}
198+
} else {
199+
if (appDelegateClassName) { // Key exists but is empty or not a string
200+
NSLog(@"Firebase Warning: Info.plist key '%@' is present but invalid (empty or not a string: %@). Proceeding with default setDelegate: swizzling.", kFirebaseAppDelegateClassNameKey, appDelegateClassName);
201+
} else { // Key does not exist
202+
NSLog(@"Firebase: Info.plist key '%@' not found. Proceeding with default setDelegate: swizzling.", kFirebaseAppDelegateClassNameKey);
203+
}
204+
}
205+
206+
// Original swizzling logic if the Info.plist key is not used or class not found
166207
Class uiApplicationClass = [UIApplication class];
167208
SEL originalSelector = @selector(setDelegate:);
168209
Method originalMethod = class_getInstanceMethod(uiApplicationClass, originalSelector);
@@ -172,18 +213,11 @@ + (void)load {
172213
return;
173214
}
174215

175-
// Replace the original method's implementation with Firebase_setDelegate
176-
// and store the original IMP.
177216
IMP previousImp = method_setImplementation(originalMethod, (IMP)Firebase_setDelegate);
178217
if (previousImp) {
179218
g_original_setDelegate_imp = previousImp;
180219
NSLog(@"Firebase: Successfully swizzled [UIApplication setDelegate:] and stored original IMP.");
181220
} else {
182-
// This would be unusual - method_setImplementation replacing a NULL IMP,
183-
// or method_setImplementation itself failed (though it doesn't typically return NULL on failure,
184-
// it might return the new IMP or the old one depending on versions/runtime).
185-
// More robustly, g_original_setDelegate_imp should be checked before use.
186-
// For now, this logging indicates if previousImp was unexpectedly nil.
187221
NSLog(@"Firebase Error: Swizzled [UIApplication setDelegate:], but original IMP was NULL (or method_setImplementation failed to return the previous IMP).");
188222
}
189223
});
@@ -195,25 +229,77 @@ + (void)load {
195229
namespace util {
196230

197231
void RunOnAppDelegateClasses(void (^block)(Class)) {
198-
if (g_seen_delegate_classes_count > 0) {
199-
NSLog(@"Firebase: RunOnAppDelegateClasses executing block for %d already seen delegate class(es).",
200-
g_seen_delegate_classes_count);
201-
for (int i = 0; i < g_seen_delegate_classes_count; i++) {
202-
if (g_seen_delegate_classes[i]) { // Safety check
203-
block(g_seen_delegate_classes[i]);
232+
if (g_firebase_specific_delegate_mode) {
233+
// Specific Delegate Mode
234+
if (g_firebase_target_app_delegate_class) {
235+
// Check if the target delegate is among the "seen" classes (it should be g_seen_delegate_classes[0])
236+
// This also implies g_seen_delegate_classes_count == 1 due to how +load sets it up.
237+
if (g_seen_delegate_classes_count == 1 && g_seen_delegate_classes[0] == g_firebase_target_app_delegate_class) {
238+
NSLog(@"Firebase: RunOnAppDelegateClasses (Specific Mode) - Applying block for target delegate: %@", NSStringFromClass(g_firebase_target_app_delegate_class));
239+
block(g_firebase_target_app_delegate_class);
240+
241+
// For pending blocks: these should also only run for the target delegate.
242+
// If RunOnAppDelegateClasses is called multiple times, pending blocks should only execute once for the target.
243+
static dispatch_once_t once_token_specific_delegate_pending_blocks;
244+
dispatch_once(&once_token_specific_delegate_pending_blocks, ^{
245+
if (g_pending_block_count > 0) {
246+
NSLog(@"Firebase: RunOnAppDelegateClasses (Specific Mode) - Executing %d pending block(s) for target delegate: %@", g_pending_block_count, NSStringFromClass(g_firebase_target_app_delegate_class));
247+
for (int i = 0; i < g_pending_block_count; i++) {
248+
if (g_pending_app_delegate_blocks[i]) {
249+
g_pending_app_delegate_blocks[i](g_firebase_target_app_delegate_class);
250+
g_pending_app_delegate_blocks[i] = nil; // Clear after execution
251+
}
252+
}
253+
g_pending_block_count = 0; // All pending blocks consumed for the specific target
254+
}
255+
});
256+
// Do NOT add the current 'block' to g_pending_app_delegate_blocks in this mode after the initial processing of pending blocks.
257+
// Each new call to RunOnAppDelegateClasses with a 'block' will execute it immediately on the target.
258+
// If pending blocks haven't been processed yet (before dispatch_once runs), new blocks might need queuing.
259+
// However, the dispatch_once ensures pending blocks are processed once. A new block passed to this function
260+
// will execute above. If it needs to be "pending" for the specific delegate, it implies the specific delegate
261+
// itself isn't "active" yet, which contradicts this mode.
262+
// The logic is: if specific delegate is known, all blocks run on it. Pending is for when it's not yet known.
263+
} else {
264+
// This state implies g_firebase_target_app_delegate_class is set, but it's not yet in g_seen_delegate_classes,
265+
// or g_seen_delegate_classes is not correctly set to [target, nil, ...] count 1.
266+
// This might happen if RunOnAppDelegateClasses is called *very* early, even before +load fully configures g_seen_delegate_classes for specific mode.
267+
// In this scenario, we should queue the block.
268+
NSLog(@"Firebase: RunOnAppDelegateClasses (Specific Mode) - Target delegate %@ not yet fully processed or g_seen_delegate_classes mismatch. Queuing block.", NSStringFromClass(g_firebase_target_app_delegate_class));
269+
if (g_pending_block_count < MAX_PENDING_APP_DELEGATE_BLOCKS) {
270+
g_pending_app_delegate_blocks[g_pending_block_count] = [block copy];
271+
g_pending_block_count++;
272+
NSLog(@"Firebase: RunOnAppDelegateClasses (Specific Mode) - added block to pending list (total pending: %d).", g_pending_block_count);
273+
} else {
274+
NSLog(@"Firebase Error: RunOnAppDelegateClasses (Specific Mode) - pending block queue full. Discarding block.");
275+
}
204276
}
277+
} else {
278+
// Should not happen if +load correctly sets g_firebase_target_app_delegate_class when g_firebase_specific_delegate_mode is true.
279+
NSLog(@"Firebase Error: RunOnAppDelegateClasses (Specific Mode) - Target delegate class is nil. Block not executed or queued.");
205280
}
206281
} else {
207-
NSLog(@"Firebase: RunOnAppDelegateClasses - no delegate classes seen yet. Block will be queued for future delegates.");
208-
}
282+
// Original Swizzling Mode (existing logic)
283+
if (g_seen_delegate_classes_count > 0) {
284+
NSLog(@"Firebase: RunOnAppDelegateClasses (Swizzle Mode) executing block for %d already seen delegate class(es).",
285+
g_seen_delegate_classes_count);
286+
for (int i = 0; i < g_seen_delegate_classes_count; i++) {
287+
if (g_seen_delegate_classes[i]) { // Safety check
288+
block(g_seen_delegate_classes[i]);
289+
}
290+
}
291+
} else {
292+
NSLog(@"Firebase: RunOnAppDelegateClasses (Swizzle Mode) - no delegate classes seen yet. Block will be queued for future delegates.");
293+
}
209294

210-
// Always try to queue the block for any future new delegate classes.
211-
if (g_pending_block_count < MAX_PENDING_APP_DELEGATE_BLOCKS) {
212-
g_pending_app_delegate_blocks[g_pending_block_count] = [block copy];
213-
g_pending_block_count++;
214-
NSLog(@"Firebase: RunOnAppDelegateClasses - added block to pending list (total pending: %d). This block will run on future new delegate classes.", g_pending_block_count);
215-
} else {
216-
NSLog(@"Firebase Error: RunOnAppDelegateClasses - pending block queue is full (max %d). Cannot add new block for future execution. Discarding block.", MAX_PENDING_APP_DELEGATE_BLOCKS);
295+
// Always try to queue the block for any future new delegate classes in swizzle mode.
296+
if (g_pending_block_count < MAX_PENDING_APP_DELEGATE_BLOCKS) {
297+
g_pending_app_delegate_blocks[g_pending_block_count] = [block copy];
298+
g_pending_block_count++;
299+
NSLog(@"Firebase: RunOnAppDelegateClasses (Swizzle Mode) - added block to pending list (total pending: %d). This block will run on future new delegate classes.", g_pending_block_count);
300+
} else {
301+
NSLog(@"Firebase Error: RunOnAppDelegateClasses (Swizzle Mode) - pending block queue is full (max %d). Cannot add new block for future execution. Discarding block.", MAX_PENDING_APP_DELEGATE_BLOCKS);
302+
}
217303
}
218304
}
219305

0 commit comments

Comments
 (0)