views:

1361

answers:

4

I want to create a subclass of UINavigationController which always starts with the same root controller. Nothing special, so (to me) it makes perfect sense to override the init method like this:

- (id) init {
   rootController = [[MyController alloc] init];

   if (self = [super initWithRootViewController:rootController]) {
       // do some more initialization
   }

   return self;
}

This obviously creates a problem, because [super initWithRootViewController] will call [UINavigationController init], which is of course our overridden init method, so infinite recursion will occur.

I don't want to create an init method with a different name like "initCustom".

There's currently only one solution I can come up with, but I really hate this kind of hack:

- (id) init {
   if (initCalled)
       return self;

   initCalled = YES;

   rootController = [[MyController alloc] init];

   if (self = [super initWithRootViewController:rootController]) {
       // do some more initialization
   }

   return self;
}

So my question is: is there a better way of handling this? I'm sure I'm missing something very obvious, but I don't see it.

EDIT: The reason why I want to do this, as can be seen in one of my comments below:

I want to create a navigation controller that always starts with a specific view controller. I want to hide this from the consumer of the class. No need to expose things that don't need exposing. Makes life a lot easier, and code a lot cleaner (one of the reasons encapsulation was invented, right?)

+4  A: 

Did you actually test this code? Why should [super initWithRootViewController] call the overriden init method? It will call the [super init] method, which is (as you said) [UINavigationController init] (not your overriden init).

Alex
+1: `[super init] != [self init]`
Ben S
Internally, there's big chances that UINavigationController call init from initWithRootViewController.
gcamp
I actually did test this. The first code results in a stack overflow due to infinite recursion.
Philippe Leybaert
`[UINavigationController initWithRootViewController]` actually calls `[self init]`, instead of `[super init]`
Philippe Leybaert
It probably calls its own init method to reuse/share that code.
Mike Weller
@Mike: what were they thinking? That's like violating every OO rule in the book.
Philippe Leybaert
I don't understand why Apple did this. Common initialization code should not be put in a public constructor, but should be placed in a private method which is callled from every constructor
Philippe Leybaert
That is why they specifically said NOT TO SUBCLASS this class. Anyway, since initWithRootViewController: is the designated initializer, you shouldn't have any init method defined anyway. Do everything you need to do within initWithRootViewController: and you won't have a recursion problem.
Jason Coco
+5  A: 

First of all UINavigationController is not intended for subclassing.

Anyway, the simplest way to do that is to override initWithRootViewController:

- (id) initWithRootViewController:(UIViewController) viewController {
   return [super initWithRootViewController:[[[MyController alloc] init] autorelease]];
}

You better don't autorelease MyController, but you understand the idea...

gcamp
Why would UINavigationController not be intended for subclassing?
Philippe Leybaert
From the docs 'This class is not intended for subclassing. Instead, you use instances of it as-is in situations where you want your application’s user interface to reflect the hierarchical nature of your content.' This mean that Apple can change the internal behavior at any moments. (Just subclass 'init' is not bad, that's more for hardcore subclass)
gcamp
+3  A: 

Just override and use -initWithRootViewController: designated initializer. You can pass nil as argument

- (id) initWithRootViewController:(UIViewController*)ignored {
   rootController = [[MyController alloc] init];

   if (self = [super initWithRootViewController:rootController]) {
       // do some more initialization
   }

   return self;
}

Read more here: http://developer.apple.com/mac/library/documentation/cocoa/conceptual/ObjectiveC/Articles/ocAllocInit.html#//apple%5Fref/doc/uid/TP30001163-CH22-106376

vaddieg
Of course you can, but that's not very elegant if you force the consumer of this class to initialize the object in a non-standard way (i.e. by just calling `init`)
Philippe Leybaert
Always override the designated initializer. You can make it cleaner by creating a class method to "hide" the specific initializer call.
Marc Charbonneau
Acutually, you don't need to subclass UINavigationController just to add custom initialization that encapsulate your "MyViewController" as root.Just add a Category to the UINavigationController, something like:@interface UINavigationController(MyRootExtension)+(UINavigationController*)newControllerWithMyRoot;@end
vaddieg
+2  A: 

I've read a lot of things here on WHY this behaves like this, but no real clean solution. As Gcamp pointed out, the documentation explicitly tells us not to subclass UINavigationController.

So I started thinking: if subclassing is not allowed, that leaves us with encapsulation, which seems like an acceptable solution to solve this:

@interface MyNavigationController : UIViewController {
   UINavigationController *navController;
   UIViewController *myController;
}

Implementation:

@implementation MyNavigationController

- (id) init {
   if (self = [super init]) {
       myController = [[MyController alloc] init];
       navController = [[UINavigationController alloc] initWithRootViewController:myController];
   }

   return self;
}

- (void) loadView {
   self.view = navController.view;
}

- (void) dealloc {
   [navController release];
   [myController release];
   [super dealloc];
}

@end

I'm not an expert in Objective-C, so this may not be an the best way to do this.

Philippe Leybaert
If you really need a more true subclass you could look into message forwarding and `NSProxy`. I highly discourage you to do this. If Apple tells you not to subclass it, I wouldn't…
Georg
People have given you some solutions. They may or may not be 'clean' since you're not supposed to subclass. But in any class, if you subclass, you are supposed to do your initialization in a new, specialized designated initializer (for your subclass) or by overriding the parent's designated initializer. init is not the designated initializer of UINavigationController, therefore you should not be overriding it.
Jason Coco
Hearing this "explanation", it's not hard to see why many people consider Objective-C to be a very primitive language. Apple has built a wonderful framework, but it's sad it was built on top of such a messy programming language.
Philippe Leybaert
Well, it's a really old language, so. Just out of curiosity, why even create a subclass?
Jason Coco
I want to create a navigationcontroller that always starts with a specific view controller. I want to hide this from the consumer of the class. No need to expose things that don't need exposing. Makes life a lot easier, and code a lot cleaner (one of the reasons encapsulation was invented, right?)
Philippe Leybaert
In that case, I would do it the other way around. It seems like you don't want a navigation controller, you want some kind of view controller that uses navigation, ne? So create a view controller that, when it's view loads, embeds a navigation controller inside itself with itself as the root view controller.
Jason Coco
Something like that, yes. But isn't this very similar to the code I posted in this answer?
Philippe Leybaert