Hi, I'm Sam.

I like to create software, make music, and write about technology.

Find out more about me.

#uikit Posts

Customize UIKit with Method Swizzling

Posted in development, iphone, swizzling, and uikit

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!

UITableViewCell Silly Magic

Posted in development and uikit

Ever had a UITableViewCell's imageView not update when you set it's image in a callback or block? It's amazingly frustrating. I usually end up going over and over the code to make sure it sets it on the main thread, the image isn't nil, the image view is on the screen, etc, etc.

The Problem

UITableViewCells don't update when you set the imageView's image. UITableViewCell's imageView is magical and stupid. If you don't have an image in the imageView, it will nil it out and remove it from the contentView. When you set the image, it will cache it and do some silliness so your updates don't work.

The Solution

Make your own image view. Easy as that. Don't use the imageView property unless you want it to work exactly the way Apple uses it in Music.app for albums. For anything else, just make your own and add it to the contentView.

Trust me, you'll save yourself hours of frustration.