Upward Mobility: Black Magic Method Swizzling

Great hacks, great responsibility

This week, we’re going to depart from the basics, and talk about a piece of Objective-C black magic that, when used properly, can be a powerful tool. However, used incorrectly, it can cause devastation on a biblical scale. Let’s talk Method Swizzling.

Image that you have an app just about ready to go into the store, and your UI designer suddenly decides that all the UIButtons should use a different font.  Unfortunately, you have hundreds of them scattered over dozens of XIB files. Sure, you could go through and change each one by hand, but what happens if he changes his mind again?

Wouldn’t it be nice if you could programmatically force all the UIButtons in your app to use a new font? Maybe we can do it with a category? If we override the initWithCoder and initWithFrame methods of UIButton, we can call super and then set the font explicitly, right? Unfortunately, no, because super isn’t available in an overriden category method. But there is a way we can do it, using swizzling.

Swizzling involves swapping out the system version of a method for your own, and then calling the original method. The best way to understand it is by looking at an example.

#import <objc/runtime.h>
@interface UIButton (Swizzle)
@end

@implementation UIButton (Swizzle)

-(id) initWithCoderSwizzled:(NSCoder *)aDecoder {
    id me = [self initWithCoderSwizzled:aDecoder];
    if (me != NULL) {
        [me setDefaultFont];
    }
    return me;
}

-(id) initWithFrameSwizzled:(CGRect) aFrame {
    id me = [self initWithFrameSwizzled:aFrame];
    if (me != NULL) {
        [me setDefaultFont];
    }
    return me;
}

-(void) setDefaultFont {
    self.titleLabel.font = 
          [UIFont fontWithName:@"Verdana" 
               size:self.titleLabel.font.pointSize];
}

+ (void) load {
    Method method1 = class_getInstanceMethod(self, @selector(initWithCoder:));
    Method method2 = class_getInstanceMethod(self, @selector(initWithCoderSwizzled:));
    method_exchangeImplementations(method1, method2);
    method1 = class_getInstanceMethod(self, @selector(initWithFrame:));
    method2 = class_getInstanceMethod(self, @selector(initWithFrameSwizzled:));
    method_exchangeImplementations(method1, method2);
}
@end

Let’s start at the bottom, with the load method. All NSObject-based classes call their load method, if it’s defined, during application launch. The method gets a reference to the original versions of the initWithFrame and initWithCoder methods, and then uses method_exchangeImplementations to swap their definitions with our versions. In other words, the original initWithCoder now is called initWithCoderSwizzled, and vice-versa.

We can now do the equivalent of calling super by having our method call the swizzled method (which is now really the original method). After calling the base version, we can set the font.

Like any good piece of black magic, there are perils to this approach, of course. For one, you’ve now overriden every UIButton in your application, including ones in third party libraries, and even potentially ones in Apple frameworks. This may not be what you were intending. Also, there have been reports in the past of applications being rejected by Apple for using swizzling, although there are also many reports of apps being accepted without issue.

tags: , ,