Method Swizzling: Swapping Implementations at Runtime (Advanced / Specialized)

Understand what method swizzling is, how to perform it safely with method_exchangeImplementations, and why it is an advanced, specialized technique that carries real risks around collisions, thread safety, and debuggability — along with when a safer alternative should be preferred instead.

This lesson covers method swizzling, marked here explicitly as Advanced / Specialized: it is a genuinely powerful capability of the Objective-C runtime, but it is not something you should reach for casually, and most Objective-C code you write day to day should never need it. Swizzling means swapping the implementations of two methods at runtime, so that calling one selector actually runs the code originally written for a different selector. Understanding it well — both how it works and, just as importantly, why it's dangerous — is valuable even if you end up rarely using it yourself, because you will encounter it in third-party libraries, analytics SDKs, and debugging tools throughout your career, and you need to be able to reason about code that has been swizzled underneath you.

What Swizzling Actually Is

Every Objective-C method, once compiled, exists at runtime as a Method structure that pairs a selector with a function pointer — its implementation, or IMP. Ordinary message dispatch looks up the Method for a given selector on a given class and calls its IMP. Method swizzling exploits the fact that this pairing between selector and implementation is not fixed at compile time — it lives in a mutable data structure the runtime maintains, and that structure can be modified while the program is running.

Swizzling, specifically, means taking two existing methods — almost always both already implemented, often one being your own new method and the other being an existing method you don't control the source of — and swapping which implementation each one's selector points to. After the swap, sending the original selector actually runs the other method's code, and vice versa. Nothing about the call sites in your code (or in the framework code you're swizzling) changes at all; the same [view viewWillAppear:animated] call site now silently runs different code than it did before the swap.

This is almost always done inside a class's own +load method, a special class method the Objective-C runtime calls automatically, once, very early — before main() even runs, as each class is loaded into the process. Doing the swap in +load guarantees it happens exactly once, and happens before any other code has a chance to call the method being swizzled, which matters enormously for correctness, as later sections in this lesson explain.

method_exchangeImplementations: The Core Runtime Function

The actual swap is performed by the C runtime function method_exchangeImplementations(Method m1, Method m2), declared in <objc/runtime.h>. It takes two Method structures — not selectors, and not function pointers directly — and swaps their stored implementations, so that afterward, m1's selector runs what used to be m2's code, and m2's selector runs what used to be m1's code.

Those Method structures themselves are obtained with class_getInstanceMethod(Class cls, SEL selector) (or class_getClassMethod: for a class-side swap), which looks up the actual Method structure for a given selector on a given class, exactly the same lookup ordinary message dispatch performs internally. The complete pattern is therefore always the same three-step shape: look up both Method structures with class_getInstanceMethod, then hand both of them to method_exchangeImplementations in one call.

It's worth being precise about what gets swapped: it's implementations, not selectors, and not the methods' declared signatures. If the two methods being swapped have genuinely incompatible signatures — different argument types or return types — the swap still happens syntactically, but calling through it becomes undefined behavior, since the calling convention the caller expects no longer matches what the running code actually does with the stack and registers. Safe swizzling requires the two swapped methods to have matching signatures, by convention if not by compiler enforcement.

A Full Worked Example: Adding Logging to Every viewWillAppear: Call

The textbook example of swizzling is adding cross-cutting logging to a UIKit method like viewWillAppear:, without editing UIKit's source (which you don't have access to anyway) and without subclassing every view controller in an app. The pattern is to write a category on the class you want to affect, define a new method with your own logging code plus a call to what will become the original implementation, and then swap the two in +load.

Read the worked example in the code panel carefully: notice that the new method, swizzled_viewWillAppear:, calls [self swizzled_viewWillAppear:animated] on itself — which looks like infinite recursion, but isn't, precisely because of the swap. After method_exchangeImplementations runs, the selector swizzled_viewWillAppear: is bound to the ORIGINAL viewWillAppear: code, and the selector viewWillAppear: is bound to the new logging code. So the call inside the new method that appears to call itself is actually, after the swap, invoking the real original UIKit implementation — this is the standard idiom for 'calling through to the original' in a swizzle.

The effect, once this code runs once at app launch (via +load), is that every view controller in the entire app — including ones from third-party libraries you never wrote a single line of — has its viewWillAppear: calls logged, without a single line changed in any of those other classes.

Runtime Modification Is Global and Process-Wide

The single most important thing to internalize about swizzling is that it is not scoped to your own code. method_exchangeImplementations modifies the shared, process-wide method table for a class. Once you swizzle UIViewController's viewWillAppear:, every instance of every UIViewController subclass in the entire running process — your own view controllers, view controllers from Apple's frameworks, view controllers from any third-party SDK linked into the app — is affected identically. There is no way to swizzle 'just for this one screen' or 'just for objects I create'; the change applies globally, for the lifetime of the process, to every caller everywhere.

This is precisely what makes swizzling powerful for cross-cutting concerns, and precisely what makes it dangerous. It's a blunt, global instrument — closer to patching a shared library everyone links against than to overriding a method in your own subclass, which only affects instances of that specific subclass.

Legitimate Use Cases

Swizzling has a real, legitimate niche, which is exactly why it survives in production frameworks despite its risks. Cross-cutting instrumentation — automatically timing, logging, or tracing every call to a particular method across an entire app, without touching every call site — is the classic case, and is how many analytics and crash-reporting SDKs automatically capture screen views or network requests without requiring you to add a manual call in every view controller.

A second legitimate case is working around a bug in a third-party framework (including Apple's own frameworks) that you have no other way to patch, because you don't have its source and can't wait for an upstream fix. Swizzling the buggy method to wrap it with a corrected version is sometimes the only practical fix available, though it should be treated as a last resort and revisited once a real fix ships upstream.

Risks in Depth: Collisions

The most subtle real-world danger of swizzling is collisions: two entirely independent pieces of code — maybe your app's code and a third-party SDK, or two different third-party SDKs linked into the same app — each independently swizzling the same method on the same class, with no knowledge of each other. Because swizzling is a swap, not an addition, the final behavior depends entirely on the ORDER the two swaps happen in, which is often determined by unrelated factors like link order or +load invocation order that neither team controls or can predict.

In the worst case, one swizzle's 'call through to original' code ends up calling the OTHER swizzle's replacement instead of the true original implementation, because by the time it runs, the notion of 'original' has already been redefined once. This can silently produce a working-but-subtly-wrong chain, or in unluckier orderings, an infinite loop or crash — and because neither swizzling site knows the other exists, the bug can be extremely difficult to even locate, let alone fix, especially across the boundary of code you own and a closed-source SDK you don't.

Risks in Depth: Thread Safety, Debugging, and Safer Alternatives

Thread safety is the second serious risk, and it's why +load is the conventional and strongly recommended place to perform a swizzle: +load runs exactly once, very early, before the app's own main() starts and before any other code has a realistic chance to be calling the method concurrently. If a swizzle instead happened lazily, at some arbitrary later point while other threads might already be actively calling the very method being swapped, there's a genuine race: a thread could observe the method table mid-swap, in an inconsistent state, or two swizzle attempts could race each other, and the specific inconsistent states possible are difficult to reason about and to reproduce in testing.

Debugging difficulty is the third, more everyday cost. Once a method is swizzled, everything about reading the code becomes misleading: looking at a call site like [self viewWillAppear:animated] in your own view controller's source no longer tells you what will actually run, because some other file, possibly in a completely different module or a linked SDK, silently changed what that selector does, process-wide. Stepping through in the debugger shows a stack trace where the symbol names don't match the mental model you'd build from reading the call site alone, and a po self or breakpoint set on the 'real' method may not even fire, because the method you think you're calling isn't the code that's actually executing anymore.

Given all of this, swizzling should be treated as a last resort, not a first tool. Composition — building a wrapper object that holds a reference to the thing you want to add behavior around, and adds its own logic before or after delegating to it — achieves cross-cutting behavior without touching any shared global state. Delegation, using a formal @protocol and a delegate property, is the standard, safe Cocoa pattern for letting outside code hook into an object's lifecycle events. And when the type you want to modify is genuinely your own, plain subclassing and overriding the method the normal way is always safer than swizzling, since an override only affects instances of your subclass, is visible directly at the point of declaration, and never depends on load order relative to unrelated code.

Code Examples

+load runs once, automatically, before main() — the safest possible place for the swap to happen, since no other code could be mid-call on viewWillAppear: yet. class_getInstanceMethod fetches the two Method structures, and method_exchangeImplementations swaps them. The dispatch_once wrapper is extra insurance against the swap ever running twice, which would swap the implementations back to their original, unswizzled state and silently disable the whole feature.

method_exchangeImplementations(originalMethod, swizzledMethod);

Common mistake: Forgetting the dispatch_once guard: if +load somehow ran twice (e.g. the category is linked into the binary more than once), the second swap would undo the first, silently disabling the logging with no error or warning.

This is the standard, if initially confusing, idiom for swizzling: your replacement method calls the selector it was swapped WITH, not the selector it was swapped FOR, in order to preserve the original behavior. Reading this correctly requires holding the swap firmly in mind — read purely at face value, without knowing about the exchange above it, this code looks like unconditional infinite recursion.

[self swizzled_viewWillAppear:animated];

Neither File A nor File B has any idea the other exists, yet both are swizzling the exact same selector on the exact same class. Whichever +load the runtime happens to call second doesn't swap with the true original viewWillAppear: anymore -- it swaps with whatever the first swizzle already put there. The resulting chain of calls still technically works in many cases, but it now depends entirely on link order, which is exactly the kind of collision risk that makes swizzling dangerous in any codebase that includes third-party dependencies. This is precisely why swizzling should be reserved for cases with no safer alternative -- the two independent authors here would need direct coordination to reason correctly about the combined behavior, and most of the time, neither one even knows a collision occurred.

@implementation UIViewController (SwizzledLogging)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(swizzled_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

- (void)swizzled_viewWillAppear:(BOOL)animated {
    // After the swap, this call actually runs the ORIGINAL viewWillAppear: implementation.
    [self swizzled_viewWillAppear:animated];
    NSLog(@"viewWillAppear: fired for %@", NSStringFromClass([self class]));
}

@end
- (void)swizzled_viewWillAppear:(BOOL)animated {
    // This looks like it calls itself -- but after the swap above,
    // the selector swizzled_viewWillAppear: is bound to the code that
    // was ORIGINALLY viewWillAppear:'s implementation. So this line
    // actually invokes the real original UIKit behavior.
    [self swizzled_viewWillAppear:animated];

    NSLog(@"Logging: viewWillAppear fired");
}
// File A (your app code) -- unaware of File B
@implementation UIViewController (AppLogging)
+ (void)load {
    Method original = class_getInstanceMethod(self, @selector(viewWillAppear:));
    Method replacement = class_getInstanceMethod(self, @selector(appLog_viewWillAppear:));
    method_exchangeImplementations(original, replacement);
}
@end

// File B (a third-party analytics SDK) -- also unaware of File A
@implementation UIViewController (SDKTracking)
+ (void)load {
    Method original = class_getInstanceMethod(self, @selector(viewWillAppear:));
    Method replacement = class_getInstanceMethod(self, @selector(sdk_viewWillAppear:));
    method_exchangeImplementations(original, replacement);
}
@end
// Whichever +load runs SECOND ends up swapping with what the FIRST swap
// already put in place, not with the true original UIKit method -- the
// two swizzles chain together in an order neither file controls or knows about.

Where Swizzling Shows Up in Practice

Many analytics and crash-reporting SDKs use swizzling internally to automatically capture screen views (by swizzling viewDidAppear:), network requests (by swizzling NSURLSession methods), or unhandled exceptions, all without requiring the integrating app to add manual instrumentation calls everywhere. Apple's own AppKit and UIKit have, at times, been swizzled by debugging and accessibility tools to inject behavior Apple didn't expose through a public API — which is exactly the kind of last-resort, no-other-option scenario swizzling is best reserved for, and exactly why Apple's App Review guidelines and documentation caution against relying on it in shipping code where a supported API exists instead.

Performing a swizzle lazily, at some arbitrary point after app launch, possibly on a background thread.

Perform swizzles exactly once, inside +load, guarded with dispatch_once, which runs before main() and before any other code could be concurrently calling the method being swapped.

Swizzling after other code may already be actively calling the target method introduces a real race condition on the shared method table, which can produce crashes or corrupted state that are extremely difficult to reproduce.

Swizzling two methods whose signatures (argument or return types) don't actually match.

Only swizzle methods with matching signatures, and verify this carefully, since the compiler does not enforce it for you.

method_exchangeImplementations swaps implementations regardless of signature compatibility; calling through a mismatched signature is undefined behavior that can corrupt the stack or misinterpret arguments.

Reaching for swizzling to modify behavior on a class you fully own and control.

Use plain subclassing and method overriding, or composition/delegation, for any type you own.

Swizzling is a blunt, global, process-wide tool meant for classes you can't otherwise modify (like framework classes); for your own types, an override is safer, more localized, and far easier to read and debug.

Assuming a swizzle is 'scoped' to the objects your own code creates or touches.

Treat every swizzle as affecting every instance of the target class in the entire process, including ones from other modules and third-party SDKs.

method_exchangeImplementations modifies the shared class-wide method table; there is no per-instance or per-module scoping mechanism, so the effect is always global for the lifetime of the process.

What does method_exchangeImplementations actually swap? A. Two selectors' names B. Two methods' stored implementations (IMPs) C. Two classes' entire method lists D. Two objects' ivars

B. Two methods' stored implementations (IMPs) — It swaps which implementation (IMP) each of the two given Method structures points to, so each selector afterward runs what used to be the other one's code.

Why is +load the conventional place to perform a swizzle? A. It runs on a background thread automatically B. It runs exactly once, very early, before main() and before other code could be concurrently calling the swizzled method C. It is the only method the runtime allows swizzling inside of D. It automatically retries the swap if it fails

B. It runs exactly once, very early, before main() and before other code could be concurrently calling the swizzled method — Performing the swap this early avoids the thread-safety risk of swapping a method's implementation while other code might already be actively calling it.

Two unrelated pieces of code both swizzle the same method on the same class, unaware of each other. What is the core danger? A. The app fails to compile B. Both swizzles are automatically merged safely by the runtime C. The final combined behavior depends on unpredictable load/link order, and one swizzle's 'call original' may actually call the other's replacement D. Only the first swizzle takes effect and the second is silently ignored

C. The final combined behavior depends on unpredictable load/link order, and one swizzle's 'call original' may actually call the other's replacement — Because swizzling is a swap rather than an addition, order matters enormously, and two independent swizzles chain together in an order neither author controls or can predict, which can silently break the 'call original' idiom.

Why does swizzled code make debugging harder? A. Swizzled methods cannot be logged with NSLog B. Breakpoints are disabled on swizzled classes C. Reading a call site no longer reliably tells you what code actually runs, since the swap happened elsewhere, possibly in an unrelated module D. The Objective-C compiler removes debug symbols from swizzled methods

C. Reading a call site no longer reliably tells you what code actually runs, since the swap happened elsewhere, possibly in an unrelated module — Once a method is swizzled, the mapping between a selector and its implementation is no longer what the source at the call site suggests, so reasoning from reading code alone becomes unreliable.

For a class you fully own and control, which alternative to swizzling is generally safest? A. Swizzling is still the best option in all cases B. Plain subclassing and overriding the method C. Swizzling from a background thread instead of +load D. Declaring the method as @dynamic

B. Plain subclassing and overriding the method — Subclassing and overriding is scoped to instances of your subclass, visible at the point of declaration, and carries none of swizzling's global, process-wide, order-dependent risks.

What happens if you swizzle two methods with genuinely incompatible signatures (different argument or return types)? A. The compiler refuses to build B. The runtime automatically inserts type-converting glue code C. The swap still happens syntactically, but calling through it is undefined behavior D. Only the return type is swapped, arguments are left alone

C. The swap still happens syntactically, but calling through it is undefined behavior — method_exchangeImplementations swaps implementations without checking signature compatibility; a mismatch leads to undefined behavior at the calling-convention level, since the running code no longer agrees with what its caller expects.

You want to log every call to a third-party network library's -sendRequest: method, but you have no access to its source and cannot subclass the class it's defined on (it's declared final-like via being instantiated internally by the library, never subclassed by callers). Sketch the +load-based swizzle you'd write, and explain specifically why dispatch_once matters here.

Write a category on the library's class with a +load that uses class_getInstanceMethod to fetch both -sendRequest: and your new -logged_sendRequest:, then calls method_exchangeImplementations once inside a dispatch_once block. dispatch_once matters because +load can, in some linking configurations, be invoked more than once for the same category; without the guard, a second invocation would swap the implementations back, silently undoing your logging with no warning or error, which would be very hard to notice without dedicated testing.

Think about what would happen if the library itself, or some other part of your own code, happened to load and trigger +load more than once for the same category.