Customize UIKit with Method Swizzling
Posted on
Update 03/03/12: You should really use UIAppearance since this no longer works in iOS 5.0.
Have you ever wanted to override some functionality in UIKit that was in a hard to reach place? A lot of applications on the App Store have custom UINavigationBar
's. I really wanted to do this one of my company's upcoming apps.
A popular solution for this creating a category and overriding the method you want to change in it. For this the example, we'll make a UINavigationBar
green instead (obviously you could do something cool here instead). The category way would look something like this:
@interface UINavigationBar (CustomBackground)
@end
@implementation UINavigationBar (CustomBackground)
- (void)drawRect:(CGRect)rect {
[[UIColor greenColor] set];
CGRect fillRect = CGRectMake(0.0, 0.0, self.frame.size.width, self.frame.size.height);
CGContextFillRect(UIGraphicsGetCurrentContext(), fillRect);
}
@end
This approach works very well, but there are two issues with it: overriding methods with a category is an Objective-C no no and if you need to call the default implementation, you can't.
In my app, I wanted to change most of the navigation bars in all of the navigation controllers. I am using a UIImagePickerController
in part of the app and it was customized to. I really wanted to keep it the translucent style instead of the style for the rest of my app.
I decided any UINavigationBar
with UIBarStyleDefault
as its style, I want to override and everything else leave alone. There is no way to do this with the category approach. You can't call [self drawRect:rect]
because it would infinitely call itself since you replaced it with the method you are calling it from.
Method swizzling
After some googling and some help from #macdev on Freenode, I changed my solution to use method swizzling. Method swizzling, in short, is switching methods at runtime. So you can say for UINavigationBar
don't use the standard drawRect:
, but instead swap it with a different one. (This is kinda confusing, but hang in there. It's not that hard.)
I updated my category to look like this:
@interface UINavigationBar (CustomBackground)
- (void)drawRectCustomBackground:(CGRect)rect;
@end
@implementation UINavigationBar (CustomBackground)
- (void)drawRectCustomBackground:(CGRect)rect {
if (self.barStyle == UIBarStyleDefault) {
[[UIColor greenColor] set];
CGRect fillRect = CGRectMake(0.0, 0.0, self.frame.size.width, self.frame.size.height);
CGContextFillRect(UIGraphicsGetCurrentContext(), fillRect);
return;
}
// Call default implementation
[self drawRectCustomBackground:rect];
}
@end
I then updated main.m
to look like this:
#import <objc/runtime.h>
#import "UINavigationBar+CustomBackground.h"
int main(int argc, char *argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
// Swizzle the nav bar
Method drawRectCustomBackground = class_getInstanceMethod([UINavigationBar class], @selector(drawRectCustomBackground:));
Method drawRect = class_getInstanceMethod([UINavigationBar class], @selector(drawRect:));
method_exchangeImplementations(drawRect, drawRectCustomBackground);
int retVal = UIApplicationMain(argc, argv, nil, @"AppDelegate");
[pool release];
return retVal;
}
So this kinda hurt my head when I was first looking at all of this. In main.m
, before the application starts, I swizzle the UINavigationBar
methods. method_exchangeImplementations()
switches my drawRectCustomBackground:
with drawRect:
in
UINavigationBar
. When I call the default implementation in drawRectCustomBackground:
, it looks like I'm calling the same method, but I am actually calling the default implementation because it swapped them.
This is pretty crazy and a little confusing (especially with someone new to Objective-C), but really powerful. You can use this approach to customize a lot of things Apple didn't intend for you to mess with. Go out and make something cool!