One of the basic Objective-C design paradigms is Model-View-Controller.
Views is views typically have two components. There is the logic that drives a view: formatting, management of subviews, and lifecycle management. And there is appearance of the view. This might include the positioning of subviews, the color of the view and subviews, how the view scales, and so on.
Developers that need a to be able to customize their views appearance while reusing the same logic typically use a few different strategies:
-
Define new classes for each required appearance, duplicating the code that drives the logic between the classes. This leads to maintenance and code bloat issues.
-
Architect the view class so that it can be easily subclassed and modified. This can expand the scope of testing dramatically, and lead to developer error. For example, a developer might not call the super’s implementation of a method, or might not understand a nuance of the super class, which could lead to bugs. It also means the author of the parent class must keep their interfaces consistent, or risk breaking all the child classes in the code base.
-
Define properties on the view class to allow it’s appearance to be modified. As long as the customization stays simple (colors, fonts, etc) this approach works pretty well. But as the requirements grow, so does the number of possible combinations of properties, which grows the amount of testing and unit testing the developer has to do (because as a responsible developer you’re using unit tests, right?)
NIBs give developers a fourth option. You can keep your view classes specific to the logic, and let the appearance of your view be defined by the NIB. There is no requirement that a view class always be attached to the same view NIB. This frees up developers to define an appearance for a view as many times for as many contexts as they want in an application!
Better yet, if you’re writing a library or a framework, there is also no requirement that a NIB for your class be part of your library or framework! You could define the logic of a view controller, such as a login view controller for a web service, but allow the end consumer of your framework to bring their own NIB to your class to define their own appearance. Under NIB-less development practice, the developer of a framework would have to provide all the possible customization options in advance. This means considering lots of different properties, defining a lot of properties, documenting all the properties, and testing all the properties. And then repeating this series of steps if a customer asks for some sort of customization you haven’t already thought of, which might result in a lot of one-off properties for specific customers.
A final example I commonly run into on the Mac is needing to have the same logic for multiple points in a flow in an application that have slightly difference appearances. An example might again be a login for a web service. I might need to have a view for logging in to a web service as part of a setup assistant, as part of a prompt for a login failure, or as part of some sort of editing or preference function. Each has it’s own slightly tweaked UI requirements.
As you can see, reasons for using NIBs extend far beyond just using Interface Builder. NIBs allow for some insanely great customizability for your classes.
A note on Storyboards: Storyboards can perform some of the functionality of a NIB based workflow. For example, in a Storyboard I can define table cell prototypes for a table view that similarly decouple a table cell’s logic from it’s appearance. Cell prototypes in Storyboards seem to be only defined for their specific host table view controller. If you have lots of view controllers, multiple applications, or a need for a external developer to define appearance outside of your library/framework for a view, Storyboards don’t seem as capable or as ideal for this behavior. There is no reason you can’t mix Storyboards with re-definable NIB based views though!
A Sample Case
I started this example with an empty project that simply has a UITableViewController with no implementation. Feel free to follow along from that point, download the sample code, or work from an empty UITableViewController implementation in your own project.
Let’s create a new class and define a header for our table view cell:
#import <UIKit/UIKit.h>
@interface NEXTableViewCell : UITableViewCell
{
IBOutlet UILabel * valueLabel;
}
//the properties to the UILabels we want end developers to be able
//to modify
@property (strong) IBOutlet UILabel * titleLabel;
@property (strong) IBOutlet UILabel * unitLabel;
//the value to display
@property (copy) NSNumber * value;
@end
It’s important to note that I defined the outlet to the value label as a class member, but I defined the title and units label outlets as properties. This comes down to the intended use for each. I want all the outlets to show up in Interface Builder, so they all must be defined in the header, but I’m only intending for the title and units label to be accessible from code. I want to manage the text of the value label with a number formatter, but I’ll leave it to the end consumer of this class to define the text in the title and unit labels. To do so, I’ll give a consumer of this class direct access to titleLabel and unitLabel, but not valueLabel.
Time to hammer out the implementation:
#import “NEXTableViewCell.h”
@interface NEXTableViewCell ()
{
NSNumber * _value;
}
@property NSNumberFormatter * numberFormatter;
@end
@implementation NEXTableViewCell
– (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
}
returnself;
}
– (void)awakeFromNib
{
self.numberFormatter = [[NSNumberFormatteralloc] init];
self.numberFormatter.generatesDecimalNumbers = YES;
[self.numberFormattersetNumberStyle:NSNumberFormatterDecimalStyle];
[self.numberFormattersetMaximumFractionDigits:2];
[self.numberFormattersetMinimumFractionDigits:2];
[selfupdateValueText];
}
– (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];
}
-(NSNumber *)value
{
return [_value copy];
}
-(void)setValue:(NSNumber *)value
{
_value = [value copy];
[selfupdateValueText];
}
-(void)updateValueText
{
valueLabel.text = [self.numberFormatterstringFromNumber:self.value];
}
@end
Typically, when I use this sort of implementation, I actually make the number formatter an accessible property so I can redefine the logic on how numbers for formatted for each cell on the fly without having to redefine any new view classes, but we’ll hardcode it in there for this demo.
Now all that’s is to define my two NIBs and hookup the appropriate outlets.
These cells aren’t the prettiest or the most elegant looking (especially that units field far to the right, yuck!), but they’re a rough approximation of a real world situation, so they’ll do.
In the little table view cell that’s intended for the lower priority content, I’ll define a smaller cell, and hookup the outlets for all the labels. In the second NIB, I’ll define a larger cell without a title, leaving the outlet for the title label simply not hooked up. Easy enough. I use Auto Layout so that the cells can sanely deal with ambiguously sized content without me having to write any more code. It’s important to note that each NIB’s layout requirements mean very different Auto Layout constraints, which might be more difficult to implement without NIBs.
Now it’s time to write some implementation for the table view controller:
#import “NEXTableViewController.h”
#import “NEXTableViewCell.h”
@interface NEXTableViewController ()
@end
@implementation NEXTableViewController
– (void)viewDidLoad
{
[super viewDidLoad];
[self.tableView registerClass:[NEXTableViewCell class] forCellReuseIdentifier:@”TableCell”];
[self.tableView registerNib:[UINib nibWithNibName:@“LittleTableCell” bundle:[NSBundle mainBundle]] forCellReuseIdentifier:@”TableCell”];
[self.tableView registerClass:[NEXTableViewCell class] forCellReuseIdentifier:@”BigTableCell”];
[self.tableView registerNib:[UINib nibWithNibName:@“BigTableCell” bundle:[NSBundle mainBundle]] forCellReuseIdentifier:@”BigTableCell”];
}
– (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
– (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 2;
}
– (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if(section==0)
return 1;
if(section==1)
return 3;
return 0;
}
– (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NEXTableViewCell *cell = nil;
if(indexPath.section==0)
cell = [tableView dequeueReusableCellWithIdentifier:@”BigTableCell”forIndexPath:indexPath];
else
cell = [tableView dequeueReusableCellWithIdentifier:@”TableCell”forIndexPath:indexPath];
//setup the cell
//going to hardcode some data here
if(indexPath.section==0)
{
cell.titleLabel.text = @”Gold Stars”;
cell.unitLabel.text = @”Gold Stars/Walnut”;
} else {
switch (indexPath.row) {
case 0:
cell.titleLabel.text = @”Cats”;
cell.unitLabel.text = @”Fiddles/Harmonica”;
break;
case 1:
cell.titleLabel.text = @”Burgers”;
cell.unitLabel.text = @”Movies/Chair”;
break;
case 2:
cell.titleLabel.text = @”Tumbleweeds”;
cell.unitLabel.text = @”Boats/Boats”;
break;
default:
break;
}
}
//just throw a number into the cell
cell.value = @(rand()/4000.0f);
return cell;
}
– (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.section == 0)
return 110.0f;
return 55.0f;
}
@end
The titles and units are gibberish and the values random, but hey, it’ll do.
The magic to how this works is in viewDidLoad. There is no requirement that a class can only be registered for one and only one identifier. Nor is there a requirement that a NIB and a class can’t be registered for the same identifier, or that I can’t register a class with different NIBs. So I register two identifiers with the same class, but two different NIBs. It’s worth noting that I’m not doing anything strange here, the APIs were intentionally designed for exactly what I’m doing.
So, build, run, and:
Tada!
Things to note:
-
See how clean the code is? I didn’t have to define two classes, or a bunch of properties to make this happen. I’d probably need at least a dozen properties defined or a lot of properties and helper functions to get away with doing this with a single class. Or I could write a second class or subclass for the second cell style. But this code is extremely clean and maintainable.
-
The code overhead to defining a potential third style is nonexistent. I could define a third, forth, or fifth style of this cell without writing any more code.
-
I love how this style of using NIBs promotes code reusability. Another developer could come along in this project or pull my code into another project, not knowing or caring about the logic side of the implementation, define a new NIB, and continue working. If your team has savvy designers, your designers could even work directly in Interface Builder without having to know much about the code. And this approach is very flexible to requirements changes.
-
I could have probably stuck to Storyboard table cell prototypes for this specific demo, but if I was implementing this view controller or these cells in a few other places, that might get unwieldy pretty fast. A good example of this is projects where I might have to define several table view controllers in a storyboard that are different from one another, but have to host similar content. Storyboard prototypes also only work for the specific case of table view cells.
NIBs become very useful when we look at them as a specific tool for a specific situation instead of a WYSIWYG alternative to doing user interfaces purely in code. They’re a great tool in situations when your user interface requirements are producing code bloat, and when you need highly flexible and dynamic user interfaces without redefining your view logic.
It’s worth noting there are at least a few places I would not use this approach, one of which is in a framework or a library. I did mention that it is handy if the end developer is allowed to bring their own NIB, but I dislike when libraries require me to bring a NIB into my project. It’s another file to keep track of and maintain.
To deal with this, you can detect if your view is being purely loaded from code or is being loaded with a NIB. If your UIView is being loaded from a NIB, it will be initialized with initWithCoder:, but if your UIView is being initialized without a NIB, initWithFrame: will be called instead. This gives you a chance to set up a sensible default layout of a view and setup subviews if no NIB has been provided to your class. For view controllers, you will receive the awakeFromNib message in the case you have been initialized with a NIB, and you can check the nibName property in other functions to find if the user has initialized your view controller with a NIB. This is handy for when you’re creating a library and you want to give the user the option of bringing their own NIB, but you don’t want to require the user to supply their own NIB.
Conclusion
Developers should look at Interface Builder and NIBs not as an alternative to writing layouts in code, but instead as a special tool that can be used to promote flexibility in code. NIBs make several great use cases possible that a code only flow doesn’t really have, or doesn’t make easy. I’d love to see frameworks start to more standardly support giving users the option of using their own NIBs.
Grab the sample code from my Bitbucket repo!