Initializing UIViewControllers

Most code samples that Apple provides for initializing a view controller look something like this:


MyController *myController = [[MyController alloc] initWithNibName:@"MyView" bundle:nil];

This always felt like a strange pattern to me. And as Ole Begemann recently pointed out, it’s poor encapsulation. Why does the caller specify which .xib file some other controller should use? Isn’t it the target controller’s responsibility to determine that?

Fortunately, there’s a better way. When you initialize a view controller, you should just call init as you would with any other object:


MyController *myController = [[MyController alloc] init];

Then it’s up to MyController to determine how to initialize its view. If you don’t override init, iOS will by convention look for a nib with the same name as the controller (in this case MyController.xib). If that file doesn’t exist, it will look for a file specific to the platform (MyController~iphone.xib or MyController~ipad.xib).

If you like to name your nibs in a way that doesn’t match this convention, that’s OK too. I prefer to give nibs a name ending in “View”, since the nib encapsulates the code to create a view. In this case, you can implement nibName::

-(NSString *)nibName {
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        return @"MyView_iPad";
    }
    return @"MyView_iPhone";
}

10 Comments

  1. Stewart Gleadow
    January 24, 2012

    Thanks Chris. I’ve always though you should encapsulate the nib name inside the controller like this – but I’ve done it by creating a custom init method, I had no idea there were existing patterns for finding the nib.

    Do you think this has any consequences for unit testing the UIViewController?

    I usually create the controller with no nib, and inject my own dependencies for the IBOutlets that would come from the nib. I guess you can still just call initWithNibName:bundle: with nil directly.

    Reply
    • Christopher Pickslay
      January 24, 2012

      We haven’t come across any unit-testing issues related to nib loading. The simulator will load the appropriate nib, depending whether you’re testing in the iPhone or iPad simulator.

      The bigger challenge with unit testing a universal app is that sometimes the code paths are different. So we wrap device-specific tests like this:

      if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
          expect(some-ipad-state).toBeTruthy();
      } else {
          expect(some-iphone-state).toBeTruthy();
      }

      Then you have to make sure the tests are run in both simulators so all the paths are covered.

      Reply
  2. Cris Bennett
    January 24, 2012

    Given that the UIViewController documentation says that nibName, “contains the value specified at initialization time to the initWithNibName:bundle: method”, isn’t it a remote possibility that Apple may consider this pattern an undocumented API usage? It might be safer to override -init and call initWithNibName:bundle: from there with the custom nib name (as per Ole Begemann’s post). Your pattern does work, and is a little neater.

    Reply
    • Christopher Pickslay
      January 24, 2012

      It’s not an undocumented API–nibName is a public property of UIViewController. That said, I do see your point–it’s not explicitly documented that you can use this approach instead of calling the initWithNibName:bundle: initializer, and it’s possible that the superclass implementation in a future SDK could use the underlying nibName ivar instead of the property. But I think the risk of that is low, and in any case you’d find out immediately upon testing a build.

      In any case, by default init calls [initWithNibName:nil bundle:nil], so if you use the naming conventions, you don’t need to specify nib names anywhere.

      Reply
      • Cris Bennett
        January 24, 2012

        Yes, I’m probably overdoing the caution (but you do hear App Store horror stories …). I do like prefer the explicitness of specifying a nib name over relying on the naming conventions.

        Reply
  3. Peter SHINe 신동혁
    January 24, 2012

    Thank you for the nice post about UIViewController initialization.
    Actually, I prefer to leave – (id)init as it is, since there is case for using it for pure NSObject instance of UIViewController.

    Instead, I prefer to override – (id)initWithNibName:bundle: just as it was suggested when new UIViewController is added to the project but commented out when using the template.

    To make sure this method is usable for different cases, I overrode it for three cases:
    1. If the Nib name is passed, use it.
    2. If it’s nil is passed, check the nib file with the same name as the UIViewController class name in the bundle.
    3. If it’s not in the bundle, just use nil

    By implementing it like this, you can still use the original functionality of – (id)initWithNibName using different names of Nib files or just nil.

    – (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    NSString *filename = nil;
    NSString *resourcePath = nil;

    if (nibNameOrNil == nil) {
    filename = NSStringFromClass([self class]);
    resourcePath = [[NSBundle mainBundle] pathForResource:filename ofType:@”nib”]; // Should use nib instead of xib for file type

    if ([[NSFileManager defaultManager] fileExistsAtPath:resourcePath]) {
    nibNameOrNil = filename;
    }
    }

    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];

    if (self) {
    // Initialize
    }

    return self;
    }

    Reply
    • Christopher Pickslay
      January 24, 2012

      Peter-

      What you describe is basically UIViewController’s implementation of initWithNibName:bundle:, except for the check for device type, so it’s unnecessary. You should get the same behavior by just calling [super initWithNibName:nibName bundle:bundle].

      My point was that I don’t ever want the caller to pass my controller a nib name. The nib name is an implementation detail of the controller, which can expose explicit initialization methods if necessary. For example, let’s say your controller had a different layout when presented as a modal, defined in MyViewModal.xib. I think it’s preferable to expose an initializer like -(id)initForModal than for the caller to initialize the controller using [initWithNibName:@"MyViewModal" bundle:nil].

      Reply
      • Peter SHINe 신동혁
        January 25, 2012

        Now I see your true intention of this blog more clearly.
        Using ADDITIONAL initialization method for the purpose such as the one you’ve mentioned in last reply, I can’t help but agreeing more. Thank you.

        Reply

Leave a Reply to Stewart Gleadow

Cancel Reply