
So how do you go about creating a custom look and feel to your application? Do you need to roll your own UINavigationController? Thankfully, no. With method swizzling you can substitute function implementations for certain functions like drawRect: and easily create a very custom look. Swizzling methods creates a smaller code footprint compared to subclassing the current structure or implementing your own. I’ve created a working example of how to achieve a unique navigation style with my open source framework (see the demo project) but I’ll highlight a few snippets here to give you an idea of how things work.
Swizzling the Navigation Bar
For UINavigationBar and UIToolbar, drawRect is responsible for creating the background so swizzling this function will allow us to provide a new function to draw the background. By swizzling the method, we can also call the default implementation of a function too which is really nice if you only want to change the look of one style and not the others (ie. the default style and not the translucent style ).
#import "objc/runtime.h"
#import "objc/message.h"
void Swizzle(Class c, SEL orig, SEL new){
Method origMethod = class_getInstanceMethod(c, orig);
Method newMethod = class_getInstanceMethod(c, new);
if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
else
method_exchangeImplementations(origMethod, newMethod);
}
int main(int argc, char *argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
Swizzle([UINavigationBar class], @selector(drawRect:), @selector(TKdrawRect:));
int retVal = UIApplicationMain(argc, argv, nil, @"AppDelegate");
[pool release];
return retVal;
}
Use this snippet of code in your main.m file for your project. The UINavigationBar’s drawRect: selector will be replaced as soon as the application launches. Now we need to go ahead and create a category (again, more objective-c trickery) for UINavigationBar to append a new draw function to the class.
#import "UINavigationBar+TKCategory.h"
@implementation UINavigationBar (TKCategory)
- (void)TKdrawRect:(CGRect)rect {
if (self.barStyle == UIBarStyleDefault) {
NSArray *colors = [NSArray arrayWithObjects:
[UIColor colorWithRed:176/255.0 green:188/255.0 blue:204/255.0 alpha:1],
[UIColor colorWithRed:109/255.0 green:132/255.0 blue:162/255.0 alpha:1],nil];
[UIView drawGradientInRect:rect withColors:colors];
[[UIColor colorWithRed:45/255.0 green:54/255.0 blue:66/255.0 alpha:1] set];
CGContextFillRect(UIGraphicsGetCurrentContext(), CGRectMake(0, rect.size.height-1, rect.size.width, 1));
[[UIColor colorWithRed:205/255.0 green:213/255.0 blue:223/255.0 alpha:1] set];
CGContextFillRect(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, rect.size.width, 1));
[[UIColor colorWithRed:158/255.0 green:173/255.0 blue:193/255.0 alpha:1] set];
CGContextFillRect(UIGraphicsGetCurrentContext(), CGRectMake(0, rect.size.height-2, rect.size.width, 1));
return;
}
[self TKdrawRect:rect]; // Calls default implementation
}
@end
Setup Custom Buttons
By now you’ve realized that changing the navigation bar did little to change the appearance of the buttons. UsingĀ - (id)initWithCustomView:(UIView *)customView to create custom buttons is fine but what about those pesky back buttons? They need to fit in with the new UI look too. To create a custom back button, we can subclass UIBarButtonItem and swizzle the UINavigationController push method. The subclassed bar button I’ve created closely imitates the current bar buttons so you can easily swap interface art.
Swizzle([UINavigationController class], @selector(pushViewController:animated:), @selector(TKpushViewController:animated:));
Add this snippet to the main.m file to swizzle the UINavigationController’s method so that it sets up the back button properly.
@implementation UINavigationController (TKCategory)
- (void) TKpushViewController:(UIViewController *)viewController animated:(BOOL)animated{
if(self.navigationBar.barStyle == UIBarStyleDefault
&& [self.viewControllers count] > 0
&& viewController.navigationItem.leftBarButtonItem == nil
&& [self.topViewController isKindOfClass:NSClassFromString(@"TKViewController")]){
TKViewController *vc = (TKViewController*)self.topViewController;
if(vc.tkBackButton){
[vc.tkBackButton setStyle:TKBarButtonItemStyleBack];
[vc.tkBackButton setTarget:self.topViewController.navigationController action:@selector(popViewControllerAnimated:)];
viewController.navigationItem.leftBarButtonItem = vc.tkBackButton;
}
}
[self TKpushViewController:viewController animated:animated];
}
@end
This function will set the left navigation item of the new controller if the previous controller has setup a custom back button. The tkBackButton property is declared in TKViewController.
As a mentioned earlier, an important aspect was to provide a solution that worked as seamless as possible. The solution I’ve provided doesn’t require subclassing UINavigationController or for you to implement your own navigation controller. Although I needed to subclass UIViewController for a back button, you don’t have to provide a lot of code to existing classes to comply with the new changes. As a developer, the goal is always to cleanly leverage existing frameworks as much as possible before creating your own solution.