Extending existing classes (Method Swizzling)

Objectiv-C is very dynamic language that allows for some neat tricks. One of them is to exchange one method with another. While that doesn't sound useful at first, it allows you to "extend" or "augment" an existing method.

Introduction

In a project I was working on, I was experiencing strange occasional UI "hangs". The UI wasn't responding any more for a few seconds every now and then. I had the suspicion that this was related to writing the NSUserDefaults (i. e. calling [[NSUserDefaults standardUserDefaults] synchronize];). So I wanted to measure how long the synchronize takes. I could have added NSLogs everywhere I call synchronize, but iOS also calls that method from time to time (for example when your apps enter background because you pressed the Home button). How to catch these as well?

One solution is method swizzling. This is name for replacing a method with another. In this case, I wanted to wrap the existing synchronize so that every time it was called it would also print how long it was running. And here's how to do this:

Step 1: Create a category

First, we need to create a category so we can extend the existing NSUserDefaults class:

NSUserDefaults+Timing.h:

#import <Foundation/Foundation.h>
@interface NSUserDefaults(Timing)
@end

The interface is empty as we're not adding any user-visible methods.

Step 2: Create the wrapper method

NSUserDefaults+Timing.m:

#import "NSUserDefaults+Timing.h"
#import <objc/runtime.h> // Needed for method swizzling

@implementation NSUserDefaults(Timing)

- (BOOL)swizzled_synchronize
{
    NSDate *started;
    BOOL returnValue;
    
    started = [NSDate date];
    returnValue = [self swizzled_synchronize];
    NSLog(@"Writing user defaults took %f seconds.", [[NSDate date] timeIntervalSinceDate:started]);
    
    return returnValue;
}

@end

Now this looks like it's an endlessly recursing method as it calls itself over and over again. This is most confusing part of method swizzling and will be explained below.

Step 3: Swizzle diz!

Now that there's a wrapper method we actually need to replace the existing synchronize with our wrapper. This is done by adding the following method to our implementation:

+ (void)load
{
    Method original, swizzled;
    
    original = class_getInstanceMethod(self, @selector(synchronize));
    swizzled = class_getInstanceMethod(self, @selector(swizzled_synchronize));
    method_exchangeImplementations(original, swizzled);
}
Step 4: Profit! Let's discuss what we've done.

I'm now going to explain what the code is doing, step by step:

  • When the app is starting, our + (void)load is called very early in the app life-cycle. In fact, it's run even before main. This is a special feature of Objective-C. For details, see the documentation of +(void)load.
  • There are a few C function with which we can interface with the Object-C runtime. We're using class_getInstanceMethod to get the methods synchronize and swizzled_synchronize
  • And then we're swapping their implementations with method_exchangeImplementations. This means that when someone now calls the synchronize method, the code from swizzled_synchronize is actually executed and vice versa.

Alright, so what's happening now when synchronize is called?

  • Instead of the old code, the method that we implemented as swizzled_synchronize is called.
  • In this method, it looks like we're calling the same method again, causing and endless recursion. But by the time this line is reached the two method have been swapped. So when we call swizzled_synchronize we're actually calling the original method.
Bonus: Borders around all UIViews

Now let's use this for something more fun: the following code will draw borders around all UIViews. Views that were set up "manually" through initWithFrame: get a red border, views that were unarchived (for example as part of XIBs) get a blue border.

Using this revealed something I didn't know: the status bar at the top of an iOS app is drawn by the app itself. Proof:

UIView+Border.h:

#import <Foundation/Foundation.h>
@interface UIView(Border)
@end

**UIView+Border.m**:

```obj-c
#import "UIView+Border.h"
#import <QuartzCore/QuartzCore.h>
#import <objc/runtime.h>

@implementation UIView(Border)

- (id)swizzled_initWithFrame:(CGRect)frame
{
    // This is the confusing part (article explains this line).
    id result = [self swizzled_initWithFrame:frame];
    
    // Safe guard: do we have an UIView (or something that has a layer)?
    if ([result respondsToSelector:@selector(layer)]) {
        // Get layer for this view.
        CALayer *layer = [result layer];
        // Set border on layer.
        layer.borderWidth = 2;
        layer.borderColor = [[UIColor redColor] CGColor];
    }
    
    // Return the modified view.
    return result;
}

- (id)swizzled_initWithCoder:(NSCoder *)aDecoder
{
    // This is the confusing part (article explains this line).
    id result = [self swizzled_initWithCoder:aDecoder];
    
    // Safe guard: do we have an UIView (or something that has a layer)?
    if ([result respondsToSelector:@selector(layer)]) {
        // Get layer for this view.
        CALayer *layer = [result layer];
        // Set border on layer.
        layer.borderWidth = 2;
        layer.borderColor = [[UIColor blueColor] CGColor];
    }
    
    // Return the modified view.
    return result;
}

+ (void)load
{
    // The "+ load" method is called once, very early in the application life-cycle.
    // It's called even before the "main" function is called. Beware: there's no
    // autorelease pool at this point, so avoid Objective-C calls.
    Method original, swizzle;
    
    // Get the "- (id)initWithFrame:" method.
    original = class_getInstanceMethod(self, @selector(initWithFrame:));
    // Get the "- (id)swizzled_initWithFrame:" method.
    swizzle = class_getInstanceMethod(self, @selector(swizzled_initWithFrame:));
    // Swap their implementations.
    method_exchangeImplementations(original, swizzle);

    // Get the "- (id)initWithCoder:" method.
    original = class_getInstanceMethod(self, @selector(initWithCoder:));
    // Get the "- (id)swizzled_initWithCoder:" method.
    swizzle = class_getInstanceMethod(self, @selector(swizzled_initWithCoder:));
    // Swap their implementations.
    method_exchangeImplementations(original, swizzle);
}

@end