Devin's Heaven

Create A Custom UINavigationBar Style

Apple's UINavigation framework is the easiest way to build practically any standard UI app but the problem is you can't customize the aesthetic design of the bar other than the tint. You're stuck with the same glossy navigation bar that you see in every other application. Boring, right? Some applications, notably Pastebot, Weet and most recently Path have effectively layered on their own UI that fit into their application nicely. I'll show you exactly how to create your own custom navigation UI using some objective-c magic.

custom_nav.png

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.


*


25 Responses

  1. Chris Parker

    Method swizzling classes you don’t own is an extremely bad idea, especially if you are method swizzling the classes built into UIKit.

    Please don’t do this or advocate it; it’s almost guaranteed to break in any other version of iOS as Apple changes UIKit.

  2. Devin

    I think anyone willing to use swizzling should understand that their code can break b/c of uikit changes but I wouldn’t discourage it when done in a tasteful manner. I could have achieved a custom ui without swizzling and it still could break from uikit changes anyway.

  3. Evan Doll

    True, there are other ways of customizing your app that could break due to UIKit changes.

    But swizzling is BY FAR the most dangerous, error-prone, likely to burst into flames way of doing it.

    It’s fun for an experiment/learning exercise, but you should NEVER ship it.

    And recommending it in a blog post without any word of caution is unfortunate.

  4. KPR

    Swizzling methods on a class you don’t own (especially UIKit classes) is not doing it in a tasteful manner.

    You really shouldn’t advocate this. It would be great for you to write instead about customizing a UINavigationController in a sound, conventional way.

  5. mmalc

    *You* may understand why your application breaks or fails in an “interesting way” — your customers will not. Advocating this approach is doing them, and the platform as a whole, a disservice.

  6. Collin Beck

    Interesting, but sure am glad I read the comments.

  7. Collin Henderson

    Yeah, method swizzling is dangerous but I don’t see anything wrong with advocating this post. The skills of swizzling can still be transferred, if say you are swizzling one of your own classes. Great post Devin!

  8. Mike Rundle

    Very much agree with others opinions re: swizzling. It’s incredibly unsafe, and posting a tutorial about it will certainly put newer Cocoa developers down the wrong path for customizing the built-in controls and components. I truly hope you have never used this technique in shipping apps.

    For the custom buttons, why would you need to use anything more complex than subclassing UIButton (which is a UIView) and then setting that as the custom view? For custom back buttons I set its action to a method that pushes the hierarchy back:

    [[self navigationController] popViewControllerAnimated:YES];

    Then I set up my new button:

    self.navigationItem.leftBarButtonItem = backButton;
    self.navigationItem.hidesBackButton = YES;

    No swizzling needed, and it’s a totally custom back (or anything else) button in a UINavigationBar.

  9. Gi-lo

    Sure in this case swizzling is not the best idea, though it is save as “drawRect:” is always the same for drawing content to a view. You’ve better used a category which would be even less work. But I’m sure you know that.

    In some cases swizzling is the best way to customize some UIKit elements. Let’s say you want to change the white view which gets drawn in a UITabBar behind the selected glyph. Easiest way is to find the view and swizzle drawRect: like Devin does. This is the only way apple may approve it (Yes you can do it in a different way by adding your own view which is much much much more work :P )

  10. Stephen

    You can get around method swizzling with this by creating a class derived from UINavigationBar, changing the class type for the nav bar in IB, and then overriding the drawRect method.

  11. monowerker

    Just wanted to +1 everyone saying this is a bad idea. And even if it wasn’t so prone to break it is still a very opaque solution to modifying built-in behavior. Sometimes subclassing is the right solution.

  12. Dwight

    Swizzling is bad in this case, but I always got the vibe that subclassing and overriding drawRect: was prone to failure too. It may not be as fragile or dangerous, but is that really the best way to customize a UINavigationBar right now and for the future? and then further what if one wants to customize UITabBar, like Game Center? Should you build your own, subclass and override drawRect:? I’d appreciate any pointers to the right path.

  13. Jonathan Penn

    Swizzling is like a chainsaw. It’s an important feature of Objective C but it’s also able to cut your arm off. Learning how to swizzle can be indeed helpful in your own class libraries.

    If you want to achieve this kind of effect *without* swizzling and you use interface builder nibs, you can use plain old subclassing.

    Create a subclass of UINavigationController that overrides the drawRect method to do what you need to do after you delegate to the superclass.

    Then in your nib, choose the navigation controller object and in the inspector, make it’s class an instance of your subclass and not the default UINavigationController. Build and go and voila!

    For what it’s worth, subclassing is dangerous in a similar way to swizzling. The superclass could change behavior and break your subclass, but at least only the subclass is broken. The concerns about the superclass are still valid because the original class is altered. At least with subclassing, you may be able to gracefully degrade if methods you expect to call aren’t there or are doing something different now.

    * Disclaimer: I’ve used the subclassing method I mentioned above in my apps.

  14. Nic

    Hey guys,
    Could someone point or explain to me what is wrong with swizzling methods?

  15. Devin

    Suggesting that subclassing UINavigationController to create a custom navigation bar is overlooking the fact that UINavigationBar actually draws the background and not the controller. UINavigationBar is a readonly property of UINavigationController. The navigation bar variable is also under the @package directive so it accessible only to uikit anyway so I’m not sure subclassing would work. If someone can successfully pull it off, I’d like to see.

    You could just not swizzle the method and just override draw rect from the category but you’d lose the ability to call the default implementation.

  16. Nick

    We’ve used method swizzling for years in many shipping iOS apps with zero problems. We carefully consider the risks first and insert many runtime safety checks. But it’s still a public API. As with anytime you make assumptions about other folks’ code, you just need to be smart about it.

    Now, would I recommend this technique to others? Probably not. I am comfortable with the risks personally but if you haven’t figured out and fully understand this code yourself then you probably shouldn’t be using it.

  17. Collin Henderson

    I mean any good app developer should continue to update his app when SDK updates occur anyway, so as long as you keep an eye on the API’s I feel there is no harm in using this method to pull off the custom UIKit appearances.

  18. Philip McBride

    I love a good controversy. You should definitely have a big warning added about the dangers. And it would be great to show the safer alternatives of subclassing components at least (see Mike’s example). And if the whole thing can be done via subclassing, even better. Talking about how to do things under the covers is always fun. But bigger power tools need warnings attached.

  19. Nick

    Also I should add that I don’t know of any way to subclass the UINavigationBar that UINavigationController creates for itself. So unless you want to write your own UINavigationController, you’re SOL with the subclassing approach. I’d love to be proven wrong though; I would prefer not to have to swizzle of course.

  20. Gi-lo

    Sure it is possible ;)

    You need to overwrite:
    “- (UINavigationBar *)navigationBar {

    in the subclass so that the controller is returning yours instead of the default one.

    Example: http://cl.ly/2c3Z3o0w3V3u1C1X3x2P

  21. Bartosz Ciechanowski

    I’ve been bothered with this problem lately as well. Most of use have probably tried adding subview, this fails hard and is unpredictable. Having played with CALayers lately I’ve given addSublayer method a spin. It still didn’t fix the problem, most likely because deep inside adding a subview means adding sublayer (mu guess at least). However the zPosition property came to help. I set it to -5.0 and it works wonderfully so far. It always stays under labels and buttons, so it’s a good way to add custom background with using the drawRect category. However, I haven’t tasted all possible cases, it might not be as perfect. Here’s the code:

    CALayer *test;
    test = [CALayer layer];
    test.frame = CGRectMake(10, 10, 260, 20);
    test.backgroundColor = [UIColor redColor].CGColor;
    test.zPosition = -5.0;
    [myNavigationController.navigationBar.layer addSublayer:test];

  22. Pinoy Neophyte

    i think he method is too risky! but having a custom ui navigation bar style is worth a shot!

  23. Adding support for iOS 5 | Barnacle Games

    [...] many methods to implement custom navbars in iOS 4 and earlier, but each has potential side-effects, controversy, and “correctness”. I used the category method which overrides UINavigationBar’s [...]

  24. Rodolfo Goncalves

    Wy would you buy a Ferrari if you can’t drive one? As a developer, you must assume the risks that you take, and decide what is better for your project. If you’re changing the behavior of a Apple (or someone else) component, they are in charge of allow or not it. Write good code, that works, and make incredible things. Is that what counts.

  25. Angel

    I love this topic! Many people saying that method swizziling is dangerous but I will argue that the only thing dangerous there is doing it without the correct knowledge and runtime checks. We have shipped apps with this without problems, and of course that it could stop working on some new iOS (specially if you are swizzingling private APIs), but almost every solution eventually will do and with the proper checks no crashes should happen.
    Furthermore, subclassing in this case is not an option because it is the navigationBar the one that draws but it is instantiated and managed by the navigationcontroller (readonly).
    A category overriden the draw method is as dangerous as the method swizziling (it can also stop working properly with new iOS) and you lose the original implementation of the drawRect! not an option if you need the default look and feel in some bars and a very bad decision in terms of code reuse!

    In theses cases, I think that method swizzling is a great approach, but of course you have to be very careful about what you are doing and sanitize it properly.

    Anyway, I want to add another different way to achieve the same results without the method swizziling: http://sebastiancelis.com/2012/03/05/subclassing-hard-to-reach-classes/

    Good article!
    Cheers!


Leave a Comment