Skip to main content

Method Swizzling

A plain-English guide to Objective-C runtime method swizzling — what it actually does under the hood, when you'd ever reach for it, and how to do it without shooting yourself in the foot.

Method swizzling sounds scarier than it is. At its core, it’s just swapping two function pointers in a table at runtime. But because it touches the Objective-C runtime directly, it needs to be done carefully — the wrong timing or a missing super call can produce bugs that only appear in production.


Method Swizzling

What it is: swapping the implementation (IMP) of one method with another’s at runtime, so that every future call to the original selector executes your replacement instead.

The Objective-C runtime stores each class’s methods in a dispatch table: a map of SEL (selector — think string-keyed name) → IMP (a raw function pointer to the actual code). Swizzling reaches into that table and swaps two entries.

Why you’d use it: it lets you intercept and augment behavior in code you don’t own — a UIKit class, a third-party SDK, or even Apple’s own frameworks — without subclassing or modifying the source.

Common real-world uses:

  • Injecting analytics into viewDidAppear across every view controller without touching each one
  • Adding logging to URLSession data tasks during debugging
  • Patching a crash in a third-party library while waiting for an upstream fix
  • Implementing A/B test variants that change low-level behavior globally

How to do it:

import UIKit

extension UIViewController {
    static let swizzleViewDidAppear: Void = {
        let original = class_getInstanceMethod(UIViewController.self, #selector(viewDidAppear(_:)))!
        let swizzled = class_getInstanceMethod(UIViewController.self, #selector(swizzled_viewDidAppear(_:)))!
        method_exchangeImplementations(original, swizzled)
    }()

    @objc func swizzled_viewDidAppear(_ animated: Bool) {
        // This calls the original viewDidAppear because the implementations
        // are now swapped — self.swizzled_viewDidAppear IS the original.
        swizzled_viewDidAppear(animated)
        print("[\(type(of: self))] viewDidAppear")
    }
}

Trigger the swap once at app launch, typically in AppDelegate.application(_:didFinishLaunchingWithOptions:):

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    UIViewController.swizzleViewDidAppear
    return true
}

The static let with a closure ensures the swap only ever happens once, no matter how many times the property is accessed — the Swift runtime guarantees static let initialization is thread-safe and runs exactly once.


IMP (Implementation Pointer)

What it is: a bare C function pointer that points to the actual machine code for a method. Its type is IMP, defined as typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...).

Why you care: when you call method_exchangeImplementations, you’re swapping these pointers in the class’s method table. After the swap, the selector viewDidAppear: points to your function’s code, and your selector swizzled_viewDidAppear: points to the original UIKit code.

How to think about it: imagine a dictionary ["viewDidAppear:" → codeBlock_A, "swizzled_viewDidAppear:" → codeBlock_B]. After swizzling it becomes ["viewDidAppear:" → codeBlock_B, "swizzled_viewDidAppear:" → codeBlock_A]. Calling self.swizzled_viewDidAppear(animated) inside your swizzled method now actually runs codeBlock_A — the original — which is exactly what you want.


SEL (Selector)

What it is: a compiled, interned string that uniquely identifies a method name. #selector(viewDidAppear(_:)) compiles to a SEL. Two selectors are equal if and only if their string representations are identical.

Why you care: selectors are the keys in the method dispatch table. class_getInstanceMethod takes a SEL to look up the Method struct you then pass to method_exchangeImplementations.

How it’s used in swizzling:

let originalSEL = #selector(UIViewController.viewDidAppear(_:))
let swizzledSEL = #selector(UIViewController.swizzled_viewDidAppear(_:))

let original = class_getInstanceMethod(UIViewController.self, originalSEL)
let swizzled = class_getInstanceMethod(UIViewController.self, swizzledSEL)

@objc dynamic

What it is: two Swift keywords that opt a Swift method into the Objective-C runtime’s dynamic dispatch mechanism.

  • @objc exposes the method to the Objective-C runtime and gives it a selector.
  • dynamic tells the Swift compiler to always go through Objective-C message dispatch (i.e. look up the IMP at runtime) instead of calling the method directly. Without dynamic, the compiler may inline or devirtualize the call, bypassing the swizzled dispatch table entirely.

Why you care: pure Swift methods are dispatched statically or through a Swift vtable — they never touch the Objective-C method table. That means swizzling them does nothing. A method must be @objc dynamic (or be on an NSObject subclass where the method is already @objc) for swizzling to have any effect.

How to apply it:

class MyService: NSObject {
    // Pure Swift — swizzling this has no effect
    func fetchData() { }

    // @objc only — may still be optimized away
    @objc func fetchDataObjc() { }

    // Both: safe to swizzle
    @objc dynamic func fetchDataSwizzlable() { }
}

class_getInstanceMethod vs class_getClassMethod

What they are: two runtime functions for looking up method entries from a class’s dispatch table.

  • class_getInstanceMethod returns the Method for a method called on instances (the normal case: myVC.viewDidAppear(true)).
  • class_getClassMethod returns the Method for a method called on the class itself (UIViewController.classMethod()).

Why you care: swizzling the wrong one means your swap silently does nothing. If you’re intercepting +initialize or a class-level factory method, use class_getClassMethod. For almost everything else, use class_getInstanceMethod.


method_exchangeImplementations

What it is: the single runtime function that performs the swap. It takes two Method values and atomically exchanges their IMP pointers.

Why you care: it’s atomic, so the swap is thread-safe at the pointer level. However, the window between “before the swap” and “during the first call after the swap” is not protected — which is why you should always do swizzling at app launch before any concurrent code can call the method.

How to use it:

// Both must be non-nil or you'll crash
guard
    let original = class_getInstanceMethod(UIViewController.self, #selector(viewDidAppear(_:))),
    let swizzled = class_getInstanceMethod(UIViewController.self, #selector(swizzled_viewDidAppear(_:)))
else { return }

method_exchangeImplementations(original, swizzled)

Always guard against nil before calling this. If the method doesn’t exist (wrong class, typo in selector, method only exists in a subclass), class_getInstanceMethod returns nil and force-unwrapping crashes at startup.


The Recursive-Looking Super Call

What it is: the idiom where your swizzled implementation calls itself by name, which actually invokes the original method thanks to the swapped IMP table.

Why it confuses people: it looks like infinite recursion, but it isn’t. After method_exchangeImplementations, the selector swizzled_viewDidAppear: points to the original UIKit code. So self.swizzled_viewDidAppear(animated) is dispatching to the original implementation, not looping.

How to keep it clear in your head: think of the selector as a label on a box, and the IMP as the contents. After swizzling, the label swizzled_viewDidAppear: has the original UIKit contents. Calling that label runs the original code.

@objc func swizzled_viewDidAppear(_ animated: Bool) {
    swizzled_viewDidAppear(animated)  // dispatches to ORIGINAL viewDidAppear
    Analytics.track(screen: type(of: self))
}

When NOT to Swizzle

What to watch for: swizzling is a last resort, not a first tool. Before reaching for it, consider:

  • Subclassing — if you own the code, just override the method.
  • Delegation / protocol composition — many UIKit classes expose delegate points for exactly this kind of hook.
  • Method resolution / forwardInvocation — for more targeted interception without side-effecting the whole class hierarchy.
  • Aspects or similar libraries — thin wrappers around swizzling that handle the edge cases for you.

Swizzle only when you genuinely cannot reach the call site and need to intercept it globally across a class hierarchy you don’t own.

Pitfalls to avoid:

  • Swizzling in +load (Obj-C) or early Swift initializers runs before the app delegate, which is the safest moment but also the most opaque for debugging.
  • Swizzling a method that isn’t defined on the exact class you pass (only on a superclass) means you’re swapping the superclass’s entry, affecting all subclasses — not just yours.
  • Forgetting to call the original (the “recursive” call) breaks all other behavior on that method for every object in the app.
  • Swizzling the same method twice from two different places without coordination causes one swap to cancel out the other.