Objective-C

Runtime Dynamic Method Addition and Method Swizzling in Practice: From Principles to Black Magic

By Seren  |  03 Jul, 2026  |  Leave a comment


Method Swizzling lets you replace a method implementation at runtime without touching the original source code — a technique that powers debugging tools, analytics, and AOP patterns across the iOS ecosystem. Then one day I saw a colleague using Runtime to dynamically add methods, solving a problem where “some methods are only needed in specific scenarios.” That’s when I realized how practical this stuff really is. Method Swizzling, often called “black magic,” is even more powerful. Let’s break it down from principles to real-world code.

The Underlying Structure of Methods in the Runtime

In the Objective-C Runtime, a method is represented by three components: method_name (a selector, or SEL), method_types (a type encoding string), and method_imp (a function pointer, or IMP). These are stored together in the objc_method structure.

A SEL is a method selector — essentially a hashed key derived from the method name, which uniquely identifies the method within the runtime. An IMP is a function pointer that points to the actual implementation address of the method.

Every class maintains a “method dispatch table.” Each entry in this table is a Method that maps a SEL to an IMP. The core principle of method swizzling is to modify the mapping in this table, changing the correspondence between SEL and IMP.

Dynamic Method Addition: When and How

Objective-C’s method calls are based on message sending. When an object receives a message it can’t respond to, the Runtime first calls +resolveInstanceMethod: or +resolveClassMethod:, giving the class a chance to “dynamically add a method implementation.”

#import "Phone.h"
#import <objc/runtime.h>

@implementation Phone

void sayHello(id self, SEL _cmd) {
    NSLog(@"sayHello");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if ([NSStringFromSelector(sel) isEqualToString:@"sayHello"]) {
        class_addMethod([self class], sel, (IMP)sayHello, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end

When using class_addMethod, the fourth parameter types is the method type encoding. "v@:" means: void return, first parameter is id (self), second is SEL (_cmd).

When to use dynamic method addition? The most common scenario is “lazy loading methods.” If a class has many optional methods, loading them all into memory wastes resources. You can add them dynamically when they’re actually needed. Another scenario is dictionary-to-model conversion — dynamically adding setters/getters when properties don’t exist.

The Core Implementation of Method Swizzling

The most common API for swapping is method_exchangeImplementations. There are two golden rules: do it in +load, and wrap it in dispatch_once.

#import "UIViewController+Logging.h"
#import <objc/runtime.h>

@implementation UIViewController (Logging)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method original = class_getInstanceMethod(self, @selector(viewDidAppear:));
        Method swizzled = class_getInstanceMethod(self, @selector(log_viewDidAppear:));
        method_exchangeImplementations(original, swizzled);
    });
}

- (void)log_viewDidAppear:(BOOL)animated {
    // Call the original method (it's already swizzled, so this actually calls the original viewDidAppear:)
    [self log_viewDidAppear:animated];
    // Additional tracking logic
    NSLog(@"View appeared: %@", NSStringFromClass([self class]));
}
@end

Why use +load? +load is called when the class is loaded into memory — early enough and without manual invocation. +initialize is called only when the class first receives a message. If you swizzle in +initialize, it might be called multiple times if a subclass doesn’t implement it, making the timing unreliable.

A subtle trap: Be careful when calling _cmd. After swizzling, the implementation of log_viewDidAppear: has been replaced with the original viewDidAppear: implementation. So calling log_viewDidAppear: inside this method doesn’t cause recursion — it calls the original implementation.

Advanced Practice: A Safer Swizzling Approach

Using method_exchangeImplementations directly has a hidden risk: if the current class doesn’t implement the method you’re trying to swap, the swap affects the superclass, which can cause unexpected crashes.

A safer approach is to use class_addMethod first:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method original = class_getInstanceMethod(self, @selector(viewWillAppear:));
        Method swizzled = class_getInstanceMethod(self, @selector(log_viewWillAppear:));

        // Try to add the original method using the new method's IMP
        BOOL didAddMethod = class_addMethod(self, 
                                            @selector(viewWillAppear:), 
                                            method_getImplementation(swizzled), 
                                            method_getTypeEncoding(swizzled));

        if (didAddMethod) {
            // Added successfully — the current class didn't implement viewWillAppear:
            // Point the new method to the original IMP
            class_replaceMethod(self, 
                                @selector(log_viewWillAppear:), 
                                method_getImplementation(original), 
                                method_getTypeEncoding(original));
        } else {
            // Already existed — just swap
            method_exchangeImplementations(original, swizzled);
        }
    });
}

This approach ensures that if the current class doesn’t implement viewWillAppear:, the superclass’s implementation won’t be swapped — preventing side effects on the superclass and other subclasses.

Practical Example 1: UIButton Anti-Spam

Use Method Swizzling to intercept UIControl‘s sendAction:to:forEvent: method and check the time interval in the swizzled version.

@implementation UIControl (Limit)

+ (void)load {
    Method original = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
    Method swizzled = class_getInstanceMethod(self, @selector(limit_sendAction:to:forEvent:));
    method_exchangeImplementations(original, swizzled);
}

- (void)limit_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    if (self.ignoreEvent) {
        NSLog(@"Button click intercepted");
        return;
    }
    if (self.acceptEventInterval > 0) {
        self.ignoreEvent = YES;
        [self performSelector:@selector(resetIgnoreState) withObject:nil afterDelay:self.acceptEventInterval];
    }
    [self limit_sendAction:action to:target forEvent:event];
}
@end

Practical Example 2: Safe Array Indexing

NSArray is a class cluster — you can’t swizzle NSArray directly. You need to swizzle its actual implementation class, __NSArrayI.

@implementation NSArray (CrashHandle)

+ (void)load {
    Method original = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method swizzled = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(safe_objectAtIndex:));
    method_exchangeImplementations(original, swizzled);
}

- (id)safe_objectAtIndex:(NSUInteger)index {
    if (self.count - 1 < index) {
        @try {
            return [self safe_objectAtIndex:index];
        } @catch (NSException *exception) {
            // Report crash info online
            return nil;
        }
    }
    return [self safe_objectAtIndex:index];
}
@end

Important Considerations

  • Always call the original method after swizzling: Unless you’re absolutely certain it’s safe not to, you’re likely breaking the system’s underlying logic.
  • Add prefixes to category method names: Avoid name conflicts with system or other library methods.
  • Avoid heavy operations in +load: +load runs before main, so heavy work will slow down launch time. Also, don’t initialize objects here because other classes might not be fully initialized yet.
  • Understand the call order: If both a category and the main class implement the same method, the category will override the main class — consistent with the Runtime message forwarding mechanism.

Method Swizzling is a double-edged sword. Used well, it’s black magic. Used poorly, it’s a ticking time bomb. The core rule is simple: know exactly what you’re doing, and write it with deep respect for the runtime.

Seren
Seren

A developer exploring iOS, Objective-C, Swift, and other tech stacks. Here to document my learning process, pitfalls, and growing journey across new technologies.

Your email address will not be published. Required fields are marked *