With Firebase Analytics attached, screen-entry events are logged automatically. Even though we did not add a single line of code to viewDidAppear.
The reason this works is Method Swizzling. It replaces a method’s implementation wholesale at runtime. This showcases the flexibility of the Objective-C runtime, but it is also a double-edged sword that can open the gates to debugging hell when misused.
As discussed in the objc_msgSend article, an Objective-C method call works by “finding an IMP (function pointer) through a selector and jumping to it.” Swizzling modifies this selector→IMP mapping table at runtime.
How it works: swap two rows in the mapping table
A class’s method list is a “selector → IMP” mapping table. method_exchangeImplementationsswaps the IMPs of two entries.
| Selector | IMP before swap | IMP after swap |
|---|---|---|
viewDidAppear: |
Original implementation | My implementation |
swz_viewDidAppear: |
My implementation | Original implementation |
After the swap, when the system calls viewDidAppear:, my implementation runs. The original implementation has not disappeared; it has merely moved behind the name swz_viewDidAppear:.
The complete conventional implementation
This is what the commonly used implementation looks like with all the standard safety measures included.
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [self class];
SEL originalSEL = @selector(viewDidAppear:);
SEL swizzledSEL = @selector(swz_viewDidAppear:);
Method original = class_getInstanceMethod(cls, originalSEL);
Method swizzled = class_getInstanceMethod(cls, swizzledSEL);
BOOL added = class_addMethod(cls, originalSEL,
method_getImplementation(swizzled),
method_getTypeEncoding(swizzled));
if (added) {
class_replaceMethod(cls, swizzledSEL,
method_getImplementation(original),
method_getTypeEncoding(original));
} else {
method_exchangeImplementations(original, swizzled);
}
});
}
- (void)swz_viewDidAppear:(BOOL)animated {
[self swz_viewDidAppear:animated]; // Not recursion! Explanation below
NSLog(@"Screen entry: %@", NSStringFromClass([self class]));
}
@end
There are two easily confusing points in this code.
First, [self swz_viewDidAppear:animated] is not recursion. By the time this code runs, the IMPs have already been swapped, so the swz_ selector is connected to the original implementation. In other words, this line is the “original call.” Omitting it makes the entire screen-transition logic disappear, so it is effectively mandatory.
Second, why try class_addMethod first instead of calling method_exchangeImplementations directly? Because the target method may be implemented only in the parent class, not in that class itself. Exchanging immediately would modify the parent class’s method and affect every other subclass inheriting from it. If class_addMethod succeeds, the original is newly added to “this class,” so the swap safely remains within the class’s own scope.
The reason for using +load and dispatch_once is simple. Swizzling changes global state, so it must happen exactly once, as early as possible during the app’s lifetime. The precise invocation timing of +load is covered separately in the next article (+load vs +initialize).
A quiet side effect: _cmd lies
If you print _cmd (the current selector) inside a swizzled method, you get swz_viewDidAppear: instead of viewDidAppear:. The implementation has changed, but the selector in the call path remains the same.
This is normally harmless, but it becomes a subtle bug when combined with code that uses _cmd as a key, such as the pattern of using _cmd as an Associated Objects key. If the original implementation internally depended on _cmd, its behavior may change.
How far is it safe to go?
The situations that justify swizzling are quite limited.
- Cross-screen instrumentation: when an Analytics or logging SDK automatically collects screen-entry and button-tap events
- Workarounds for third-party or system bugs: when temporarily correcting behavior in a framework whose source is unavailable
- Development debugging tools: when you want to trace every call to a particular method
Conversely, these situations should raise a red flag.
- If multiple libraries swizzle the same method, execution order depends on load order, and if one omits the original call, everything else collapses.
- When
swz_methods appear in stack traces, crash reports become difficult to interpret. - Swizzling that relies on internal system implementations can break with a single OS update.
That is why subclassing, delegate proxies, or composition should always come first when they can solve the problem. Swizzling should remain the last resort, reserved for cases where “other approaches are structurally impossible.”
What about Swift?
Pure Swift methods use static dispatch (or a vtable), so this technique does not apply. To swizzle a method, it must be exposed to the Objective-C runtime.
class Tracker: NSObject {
@objc dynamic func fire() { }
}
@objc dynamic must be present for calls to flow through the objc_msgSend path; only then can the mapping table be replaced. UIKit classes are based on Objective-C, so swizzling still works there, but as you move into the SwiftUI world, there is less room for this technique.
Summary
- Swizzling swaps the selector→IMP mapping table at runtime—the original does not disappear; it moves behind another selector.
- The
[self swz_...]call inside the swizzled implementation is not recursion; it is the original call. - The reason to try
class_addMethodfirst is to avoid touching the parent class’s method. - Use
+load+dispatch_onceso it runs only once during the app’s lifetime. - Because
_cmdmismatches, library conflicts, and OS-update risks are real, use it only as the last resort. - In Swift, only methods marked with
@objc dynamiccan be swizzled.
The next article examines another piece of runtime magic easily confused with swizzling: isa-swizzling in KVO (Key-Value Observing). It is about replacing the class itself, not a method.

