Skip to content

iOS: Allow specifying your AppDelegate class's name in the Info.plist file. #1741

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
216 changes: 151 additions & 65 deletions app/src/util_ios.mm
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
#import <UIKit/UIKit.h>
#import <objc/runtime.h>

using firebase::GetLogLevel;

// Key used in Info.plist to specify a custom AppDelegate class name.
static NSString *const kFirebaseAppDelegateClassNameKey = @"FirebaseAppDelegateClassName";

#define MAX_PENDING_APP_DELEGATE_BLOCKS 8
#define MAX_SEEN_DELEGATE_CLASSES 32

Expand All @@ -40,10 +45,12 @@ static void Firebase_setDelegate(id self, SEL _cmd, id<UIApplicationDelegate> de
Class new_class = nil;
if (delegate) {
new_class = [delegate class];
NSLog(@"Firebase: UIApplication setDelegate: called with class %s (Swizzled)",
class_getName(new_class));
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: UIApplication setDelegate: called with class %s (Swizzled)",
class_getName(new_class));
} else {
NSLog(@"Firebase: UIApplication setDelegate: called with nil delegate (Swizzled)");
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: UIApplication setDelegate: called with nil delegate (Swizzled)");
}

if (new_class) {
Expand All @@ -54,8 +61,10 @@ static void Firebase_setDelegate(id self, SEL _cmd, id<UIApplicationDelegate> de
for (int i = 0; i < g_seen_delegate_classes_count; i++) {
if (g_seen_delegate_classes[i] == current_super) {
superclass_already_seen = true;
NSLog(@"Firebase: Delegate class %s has superclass %s which was already seen. Skipping processing for %s.",
class_getName(new_class), class_getName(current_super), class_getName(new_class));
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: Delegate class %s has superclass %s which was already seen. Skipping "
@"processing for %s.",
class_getName(new_class), class_getName(current_super), class_getName(new_class));
break;
}
}
Expand All @@ -69,8 +78,9 @@ static void Firebase_setDelegate(id self, SEL _cmd, id<UIApplicationDelegate> de
for (int i = 0; i < g_seen_delegate_classes_count; i++) {
if (g_seen_delegate_classes[i] == new_class) {
direct_class_already_seen = true;
NSLog(@"Firebase: Delegate class %s already seen directly. Skipping processing.",
class_getName(new_class));
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: Delegate class %s already seen directly. Skipping processing.",
class_getName(new_class));
break;
}
}
Expand All @@ -80,12 +90,14 @@ static void Firebase_setDelegate(id self, SEL _cmd, id<UIApplicationDelegate> de
if (g_seen_delegate_classes_count < MAX_SEEN_DELEGATE_CLASSES) {
g_seen_delegate_classes[g_seen_delegate_classes_count] = new_class;
g_seen_delegate_classes_count++;
NSLog(@"Firebase: Added new delegate class %s to seen list (total seen: %d).",
class_getName(new_class), g_seen_delegate_classes_count);
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: Added new delegate class %s to seen list (total seen: %d).",
class_getName(new_class), g_seen_delegate_classes_count);

if (g_pending_block_count > 0) {
NSLog(@"Firebase: Executing %d pending block(s) for new delegate class: %s.",
g_pending_block_count, class_getName(new_class));
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: Executing %d pending block(s) for new delegate class: %s.",
g_pending_block_count, class_getName(new_class));
for (int i = 0; i < g_pending_block_count; i++) {
if (g_pending_app_delegate_blocks[i]) {
g_pending_app_delegate_blocks[i](new_class);
Expand All @@ -94,7 +106,8 @@ static void Firebase_setDelegate(id self, SEL _cmd, id<UIApplicationDelegate> de
}
}
} else {
NSLog(@"Firebase Error: Exceeded MAX_SEEN_DELEGATE_CLASSES (%d). Cannot add new delegate class %s or run pending blocks for it.",
NSLog(@"Firebase Error: Exceeded MAX_SEEN_DELEGATE_CLASSES (%d). Cannot add new delegate "
@"class %s or run pending blocks for it.",
MAX_SEEN_DELEGATE_CLASSES, class_getName(new_class));
}
}
Expand All @@ -103,7 +116,8 @@ static void Firebase_setDelegate(id self, SEL _cmd, id<UIApplicationDelegate> de

// Call the original setDelegate: implementation
if (g_original_setDelegate_imp) {
((void (*)(id, SEL, id<UIApplicationDelegate>))g_original_setDelegate_imp)(self, _cmd, delegate);
((void (*)(id, SEL, id<UIApplicationDelegate>))g_original_setDelegate_imp)(self, _cmd,
delegate);
} else {
NSLog(@"Firebase Error: Original setDelegate: IMP not found, cannot call original method.");
}
Expand Down Expand Up @@ -163,28 +177,89 @@ @implementation UIApplication (FirebaseAppDelegateSwizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *appDelegateClassName =
[[NSBundle mainBundle] objectForInfoDictionaryKey:kFirebaseAppDelegateClassNameKey];

if (appDelegateClassName && [appDelegateClassName isKindOfClass:[NSString class]] &&
appDelegateClassName.length > 0) {
Class specificClass = NSClassFromString(appDelegateClassName);
if (specificClass) {
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: Info.plist key '%@' found. Targeting AppDelegate class: %@. Swizzling "
@"of [UIApplication setDelegate:] will be skipped.",
kFirebaseAppDelegateClassNameKey, appDelegateClassName);

// Set this class as the sole "seen" delegate for Firebase processing.
// g_seen_delegate_classes_count should be 0 here in +load, but clear just in case.
for (int i = 0; i < g_seen_delegate_classes_count; i++) {
g_seen_delegate_classes[i] = nil;
}
g_seen_delegate_classes[0] = specificClass;
g_seen_delegate_classes_count = 1;
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: %@ is now the only delegate class Firebase will initially process.",
appDelegateClassName);

// If there are already blocks pending (e.g., from other Firebase components' +load
// methods), execute them now for the specified delegate. These blocks will remain in the
// pending queue, mirroring the behavior of the original swizzled setDelegate: method which
// also does not clear pending blocks after execution (as they might apply to future
// delegates). In this Info.plist mode, however, Firebase won't process further setDelegate:
// calls.
if (g_pending_block_count > 0) {
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: +load (Info.plist Mode) - Executing %d PENDING block(s) for "
@"specified delegate: %@. (Blocks are not removed from queue).",
g_pending_block_count, NSStringFromClass(specificClass));
for (int i = 0; i < g_pending_block_count; i++) {
if (g_pending_app_delegate_blocks[i]) {
g_pending_app_delegate_blocks[i](specificClass);
}
}
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: +load (Info.plist Mode) - Pending blocks executed for specific "
@"delegate.");
}
// Skip swizzling. g_original_setDelegate_imp remains NULL.
return;
} else {
NSLog(@"Firebase Error: Info.plist key '%@' specifies class '%@', which was not found. "
@"Proceeding with default swizzling.",
kFirebaseAppDelegateClassNameKey, appDelegateClassName);
}
} else {
if (appDelegateClassName) { // Key is present but value is invalid (e.g., empty string or
// wrong type).
NSLog(@"Firebase Error: Info.plist key '%@' has an invalid value ('%@'). Proceeding "
@"with default swizzling.",
kFirebaseAppDelegateClassNameKey, appDelegateClassName);
} else { // Key is not present.
// This is the default case, no special logging needed here beyond the swizzling log itself.
}
}

// Standard behavior: Swizzle [UIApplication setDelegate:]
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: Proceeding with swizzling of [UIApplication setDelegate:].");
Class uiApplicationClass = [UIApplication class];
SEL originalSelector = @selector(setDelegate:);
Method originalMethod = class_getInstanceMethod(uiApplicationClass, originalSelector);

if (!originalMethod) {
NSLog(@"Firebase Error: Original [UIApplication setDelegate:] method not found for swizzling.");
NSLog(
@"Firebase Error: Original [UIApplication setDelegate:] method not found for swizzling.");
return;
}

// Replace the original method's implementation with Firebase_setDelegate
// and store the original IMP.
IMP previousImp = method_setImplementation(originalMethod, (IMP)Firebase_setDelegate);
if (previousImp) {
g_original_setDelegate_imp = previousImp;
NSLog(@"Firebase: Successfully swizzled [UIApplication setDelegate:] and stored original IMP.");
g_original_setDelegate_imp = previousImp;
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: Successfully swizzled [UIApplication setDelegate:] and stored original "
@"IMP.");
} else {
// This would be unusual - method_setImplementation replacing a NULL IMP,
// or method_setImplementation itself failed (though it doesn't typically return NULL on failure,
// it might return the new IMP or the old one depending on versions/runtime).
// More robustly, g_original_setDelegate_imp should be checked before use.
// For now, this logging indicates if previousImp was unexpectedly nil.
NSLog(@"Firebase Error: Swizzled [UIApplication setDelegate:], but original IMP was NULL (or method_setImplementation failed to return the previous IMP).");
NSLog(@"Firebase Error: Swizzled [UIApplication setDelegate:], but original IMP was NULL (or "
@"method_setImplementation failed to return the previous IMP).");
}
});
}
Expand All @@ -196,24 +271,33 @@ + (void)load {

void RunOnAppDelegateClasses(void (^block)(Class)) {
if (g_seen_delegate_classes_count > 0) {
NSLog(@"Firebase: RunOnAppDelegateClasses executing block for %d already seen delegate class(es).",
g_seen_delegate_classes_count);
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: RunOnAppDelegateClasses executing block for %d already seen delegate "
@"class(es).",
g_seen_delegate_classes_count);
for (int i = 0; i < g_seen_delegate_classes_count; i++) {
if (g_seen_delegate_classes[i]) { // Safety check
if (g_seen_delegate_classes[i]) { // Safety check
block(g_seen_delegate_classes[i]);
}
}
} else {
NSLog(@"Firebase: RunOnAppDelegateClasses - no delegate classes seen yet. Block will be queued for future delegates.");
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: RunOnAppDelegateClasses - no delegate classes seen yet. Block will be "
@"queued for future delegates.");
}

// Always try to queue the block for any future new delegate classes.
if (g_pending_block_count < MAX_PENDING_APP_DELEGATE_BLOCKS) {
g_pending_app_delegate_blocks[g_pending_block_count] = [block copy];
g_pending_block_count++;
NSLog(@"Firebase: RunOnAppDelegateClasses - added block to pending list (total pending: %d). This block will run on future new delegate classes.", g_pending_block_count);
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: RunOnAppDelegateClasses - added block to pending list (total pending: %d). "
@"This block will run on future new delegate classes.",
g_pending_block_count);
} else {
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);
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);
}
}

Expand Down Expand Up @@ -458,24 +542,26 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func
const char *class_name = class_getName(clazz);
Method method = class_getInstanceMethod(clazz, name);
NSString *selector_name_nsstring = NSStringFromSelector(name);
const char *selector_name = selector_name_nsstring.UTF8String; // Used for logging later
const char *selector_name = selector_name_nsstring.UTF8String; // Used for logging later

IMP current_actual_imp = method ? method_getImplementation(method) : nil;

// === Begin idempotency check ===
if (current_actual_imp == imp) {
// Assuming GetLogLevel() and kLogLevelDebug are available here.
// Based on previous file content, GetLogLevel is available in this file from util_ios.h.
if (GetLogLevel() <= kLogLevelDebug) {
NSLog(@"Firebase Cache: Method %s on class %s is already swizzled with the target IMP. Skipping re-swizzle.",
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: Method %s on class %s is already swizzled with the target IMP. Skipping "
@"re-swizzle.",
selector_name, class_name);
}
return; // Already swizzled to the desired implementation

return; // Already swizzled to the desired implementation
}
// === End idempotency check ===

// If we reach here, current_actual_imp is different from imp, or the method didn't exist.
// We now assign original_method_implementation to be current_actual_imp for the rest of the function.
// We now assign original_method_implementation to be current_actual_imp for the rest of the
// function.
IMP original_method_implementation = current_actual_imp;

// Get the type encoding of the selector from a type_encoding_class (which is a class which
Expand All @@ -485,9 +571,9 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func
assert(type_encoding);

NSString *new_method_name_nsstring = nil;
if (GetLogLevel() <= kLogLevelDebug) {
NSLog(@"Firebase Cache: Attempting to register method for %s selector %s", class_name, selector_name);
}
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: Attempting to register method for %s selector %s", class_name, selector_name);

if (original_method_implementation) {
// Try adding a method with randomized prefix on the name.
int retry = kRandomNameGenerationRetries;
Expand All @@ -502,32 +588,32 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func
}
const char *new_method_name = new_method_name_nsstring.UTF8String;
if (retry == 0) {
NSLog(@"Failed to add method %s on class %s as the %s method already exists on the class. To "
@"resolve this issue, change the name of the method %s on the class %s.",
new_method_name, class_name, new_method_name, new_method_name, class_name);
NSLog(
@"Firebase Error: Failed to add method %s on class %s as the %s method already exists on "
@"the class. To resolve this issue, change the name of the method %s on the class %s.",
new_method_name, class_name, new_method_name, new_method_name, class_name);
return;
}
method_setImplementation(method, imp);
// Save the selector name that points at the original method implementation.
SetMethod(name, new_method_name_nsstring);
if (GetLogLevel() <= kLogLevelDebug) {
NSLog(@"Registered method for %s selector %s (original method %s 0x%08x)", class_name,
selector_name, new_method_name,
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: Registered method for %s selector %s (original method %s 0x%08x)",
class_name, selector_name, new_method_name,
static_cast<int>(reinterpret_cast<intptr_t>(original_method_implementation)));
}

} else if (add_method) {
if (GetLogLevel() <= kLogLevelDebug) {
NSLog(@"Adding method for %s selector %s", class_name, selector_name);
}
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: Adding method for %s selector %s", class_name, selector_name);

// The class doesn't implement the selector so simply install our method implementation.
if (!class_addMethod(clazz, name, imp, type_encoding)) {
NSLog(@"Failed to add new method %s on class %s.", selector_name, class_name);
NSLog(@"Firebase Error: Failed to add new method %s on class %s.", selector_name, class_name);
}
} else {
if (GetLogLevel() <= kLogLevelDebug) {
NSLog(@"Method implementation for %s selector %s not found, ignoring.", class_name,
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: Method implementation for %s selector %s not found, ignoring.", class_name,
selector_name);
}
}
}

Expand All @@ -541,9 +627,9 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func
selector_implementation_names_per_selector_[selector_name_nsstring];
const char *class_name = class_getName(clazz);
if (!selector_implementation_names) {
if (GetLogLevel() <= kLogLevelDebug) {
NSLog(@"Method not cached for class %s selector %s.", class_name, selector_name);
}
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: Method not cached for class %s selector %s.", class_name, selector_name);

return nil;
}

Expand All @@ -561,29 +647,29 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func
search_class = clazz;
for (; search_class; search_class = class_getSuperclass(search_class)) {
const char *search_class_name = class_getName(search_class);
if (GetLogLevel() <= kLogLevelDebug) {
NSLog(@"Searching for selector %s (%s) on class %s", selector_name,
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: Searching for selector %s (%s) on class %s", selector_name,
selector_implementation_name, search_class_name);
}

Method method = class_getInstanceMethod(search_class, selector_implementation);
method_implementation = method ? method_getImplementation(method) : nil;
if (method_implementation) break;
}
if (method_implementation) break;
}
if (!method_implementation) {
if (GetLogLevel() <= kLogLevelDebug) {
NSLog(@"Class %s does not respond to selector %s (%s)", class_name, selector_name,
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: Class %s does not respond to selector %s (%s)", class_name, selector_name,
selector_implementation_name_nsstring.UTF8String);
}

return nil;
}
if (GetLogLevel() <= kLogLevelDebug) {
NSLog(@"Found %s (%s, 0x%08x) on class %s (%s)", selector_name,
if (GetLogLevel() <= kLogLevelDebug)
NSLog(@"Firebase: Found %s (%s, 0x%08x) on class %s (%s)", selector_name,
selector_implementation_name_nsstring.UTF8String,
static_cast<int>(reinterpret_cast<intptr_t>(method_implementation)), class_name,
class_getName(search_class));
}

return method_implementation;
}

Expand Down
Loading
Loading