English 中文(简体)
Set a custom subclass of UINavigationBar in UINavigationController programmatically
原标题:

Does anyone know how can I use my custom subclass of UINavigationBar if I instantiate UINavigationController programmatically (without IB)?

Drag a UINavigationController in IB show me an under Navigation Bar and using Identity Inspectory I can change class type and set my own subclass of UINavigationBar but programmatically I can t, navigationBar property of Navigation Controller is readonly...

What should I do to customize the navigation bar programmatically? Is IB more "powerful" than "code"? I believed all that can be done in IB could be done also programmatically.

问题回答

You don t need to muck with the XIB simply use KVC.

[self.navigationController setValue:[[[CustomNavBar alloc]init] autorelease] forKeyPath:@"navigationBar"];

Since iOS5, apple provides a method to do this directly. Reference

UINavigationController *navigationController= [[UINavigationController alloc]initWithNavigationBarClass:[CustomNavBar class] toolbarClass:nil];
[navigationController setViewControllers:[NSArray arrayWithObject:yourRootViewController]];

As of iOS 4, you can use the UINib class to help solve this issue.

  1. Create your custom UINavigationBar subclass.
  2. Create an empty xib, add a UINavigationController as the single object.
  3. Set the class for the UINavigationController s UINavigationBar to your custom subclass.
  4. Set your root view controller via one of these methods:
    • [navController setViewcontrollers[NSArray arrayWithObject:myRootVC]];
    • [navController pushViewController:myRootVC];

In code:

UINib *nib = [UINib nibWithNibName:@"YourCustomXib" bundle:nil];
UINavigationController *navController = 
             [[nib instantiateWithOwner:nil options:nil] objectAtIndex:0];


Now you ve got a UINavigationController with your custom UINavigationBar.

As far as I can tell, sometimes it is indeed necessary to subclass UINavigationBar, to do some nonstandard restyling. It s sometimes possible to avoid having to do so by using categories, but not always.

Currently, as far as I know, the only way to set a custom UINavigationBar within a UIViewController is via IB (that is, via an archive) - it probably shouldn t be that way, but for now, we gotta live with it.

This is often fine, but sometimes using IB isn t really feasible.

So, I saw three options:

  1. Subclass UINavigationBar and hook it all up in IB, then muck about loading up the nib each time I wanted a UINavigationController,
  2. Use method replacement within a category to change the behaviour of UINavigationBar, rather than subclassing, or
  3. Subclass UINavigationBar and do a little mucking about with archiving/unarchiving the UINavigationController.

Option 1 was unfeasible (or at least too annoying) for me in this case, as I needed to create the UINavigationController programmatically, 2 is a little dangerous and more of a last-resort option in my opinion, so I chose option 3.

My approach was to create a template archive of a UINavigationController, and unarchive that, returning it in initWithRootViewController.

Here s how:

In IB, I created a UINavigationController with the appropriate class set for the UINavigationBar.

Then, I took the existing controller, and saved an archived copy of it using +[NSKeyedArchiver archiveRootObject:toFile:]. I just did this within the app delegate, in the simulator.

I then used the xxd utility with the -i flag, to generate c code from the saved file, to embed the archived version in my subclass (xxd -i path/to/file).

Within initWithRootViewController I unarchive that template, and set self to the result of the unarchive:

// This is the data from [NSKeyedArchiver archivedDataWithRootObject:controller], where
// controller is a CTNavigationController with navigation bar class set to CTNavigationBar,
// from IB.  This c code was created using  xxd -i 
static unsigned char archived_controller[] = {
    0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xd4, 0x01, 0x02, 0x03,
    ...
};
static unsigned int archived_controller_len = 682;

...

- (id)initWithRootViewController:(UIViewController *)rootViewController {
     // Replace with unarchived view controller, necessary for the custom navigation bar
     [self release];
     self = (CTNavigationController*)[NSKeyedUnarchiver unarchiveObjectWithData:[NSData dataWithBytes:archived_controller length:archived_controller_len]];
     [self setViewControllers:[NSArray arrayWithObject:rootViewController]];
     return [self retain];
}

Then, I can just grab a new instance of my UIViewController subclass which has the custom navigation bar set:

UIViewController *modalViewController = [[[CTNavigationController alloc] initWithRootViewController:myTableViewController] autorelease];
[self.navigationController presentModalViewController:modalViewController animated:YES];

This gives me a modal UITableViewController with a navigation bar and toolbar all set up, and with the custom navigation bar class in place. I didn t need to do any slightly-nasty method replacement, and I don t have to muck about with nibs when I really just want to work programmatically.

I would like to see the equivalent of +layerClass within UINavigationController - +navigationBarClass - but for now, this works.

I use "option 1"

Create a nib-file with only the UINavigationController in it. And set the UINavigationBar Class to my custom Class.

self.navigationController = [[[NSBundle mainBundle] loadNibNamed:@"navigationbar" owner:self options:nil] lastObject];

[navigationController pushViewController:rootViewController animated:YES];

Michael s solution works, but you can avoid NSKeyedArchiver and xxd utility. Simply Subclass UINavigationController and override initWithRootViewController, loading your custom NavigationController NIB directly:

- (id) initWithRootViewController:(UIViewController *)rootViewController
{
    [self release];
    self = [[[[NSBundle mainBundle] loadNibNamed:@"CTNavigationController" owner:nil options:nil] objectAtIndex:0] retain];  
    [self setViewControllers:[NSArray arrayWithObject:rootViewController]];
    return self;
}

Update: Using object_SetClass() no longer works as if iOS5 GM. An alternate solution has been added below.

Use NSKeyedUnarchiver to manually set the unarchive class for the nav bar.

   MyViewController *controller = [[[MyViewController alloc] init] autorelease];
   NSKeyedUnarchiver *unarchiver = [[[NSKeyedUnarchiver alloc] initForReadingWithData:[NSKeyedArchiver archivedDataWithRootObject:controller]] autorelease];
   [unarchiver setClass:[MyNavigationBar class] forClassName:@"UINavigationBar"];
   controller = [unarchiver decodeObjectForKey:@"root"];




Note: This original solution only works pre-iOS5:

There is a great solution, which I posted here -- inject navBar subclass directly into your view the UINavigationController:

#import <objc/runtime.h>

- (void)viewDidLoad {
    [super viewDidLoad];

    object_setClass(self.navigationController.navigationBar, [MyNavBar class]);
    // the rest of your viewDidLoad code
}

One scenario I ve found that we need to use subclass rather than category is to set navigationbar backgroundcolor with pattern image, because in iOS5 overwriting drawRect using category does not work any more. If you want to support ios3.1-5.0, the only way you can do is to subclass navigationbar.

These category methods are dangerous and not for novices. Also the complication with iOS4 and iOS5 being different, makes this an area that can cause bugs for many people. Here is a simple subclass that I use that supports iOS4.0 ~ iOS6.0 and is very simply.

.h

@interface XXXNavigatioNBar : UINavigationBar
@end

.m

#import "XXXNavigationBar.h"

#import <objc/runtime.h>

@implementation XXXNavigationBar

- (void) didMoveToSuperview {
    if( [self respondsToSelector: @selector(setBackgroundImage:forBarMetrics:)]) {
        //iOS5.0 and above has a system defined method -> use it
        [self setBackgroundImage: [UIImage imageNamed: @"nav-bar"]
                   forBarMetrics: UIBarMetricsDefault];
    }
    else {
        //iOS4.0 requires us to override drawRect:. BUT!!
        //If you override drawRect: on iOS5.0 the system default will break,
        //so we dynamically add this method if required
        IMP implementation = class_getMethodImplementation([self class], @selector(iOS4drawRect:));
        class_addMethod([self class], @selector(drawRect:), implementation, "v@:{name=CGRect}");
    }
}

- (void)iOS4drawRect: (CGRect) rect {
    UIImage* bg = [UIImage imageNamed:@"nav-bar-blue"];
    [bg drawInRect: rect];
}

@end

It s not recommended to subclass the UINavigationBar class. The preferred way to customizing the navigation bar is to set it s properties to make it appear as you wish and use custom views inside UIBarButtonItems along with a delegate to get the desired behavior.

What are you trying to do that needs subclassing?

Also, I don t think IB is actually replacing the navigation bar. I m pretty sure it s simply not displaying the default one and has your custom nav bar as a subview. If you call UINavigationController.navigationBar, do you get an instance of your bar?

If you want to subclass navBar just to change background image - there is no need to in iOS 5. There will be method like this one setBackgroundImage

Further to obb64 s comment, I ended up using his trick with setViewControllers:animated: to set the controller as the rootController for the navigationController loaded from the nib. Here s the code I m using:

- (void) presentModalViewControllerForClass: (Class) a_class {
  UINavigationController *navController = [[[NSBundle mainBundle] loadNibNamed: @"CustomNavBar" owner:self options:nil] lastObject];

  LoginSignupBaseViewController *controller = [[a_class alloc] initWithNibName: nil bundle: nil];
  controller.navigationController = navController;
  [navController setViewControllers: A(controller) animated: NO];

  [self presentModalViewController: navController animated: YES];

  [controller release];
}




相关问题
Asynchronous request to the server from background thread

I ve got the problem when I tried to do asynchronous requests to server from background thread. I ve never got results of those requests. Simple example which shows the problem: @protocol ...

objective-c: Calling a void function from another controller

i have a void, like -(void) doSomething in a specific controller. i can call it in this controller via [self doSomething], but i don t know how to call this void from another .m file. I want to call ...

ABPersonViewController Usage for displaying contact

Created a View based Project and added a contact to the AddressBook using ABAddressBookRef,ABRecordRef now i wanted to display the added contact ABPersonViewController is the method but how to use in ...

NSArray s, Primitive types and Boxing Oh My!

I m pretty new to the Objective-C world and I have a long history with .net/C# so naturally I m inclined to use my C# wits. Now here s the question: I feel really inclined to create some type of ...

NSUndoManager and runModalForWindow:

I have a simple Core Data app which displays a list of entities in the main window. To create or add new entities, I use a second modal window with a separate managed object context so changes can be ...

NSMutableArray values becoming "invalid"

I m trying to show a database information in a tableview and then the detailed information in a view my problem is as follow: I created a NSMutableArray: NSMutableArray *myArray = [[NSMutableArray ...

iPhone numberpad with decimal point

I am writing an iPhone application which requires the user to enter several values that may contain a decimal point (currency values, percentages etc.). The number of decimal places in the values ...

热门标签