Say you want to create a UIView
that you’re going to display in multiple places in your app.
Maybe you want to design a custom UITableViewCell
or AQGridViewCell
. Or maybe you want to create a view controller that can be displayed by several different view controllers in your app. Or maybe you want to create a custom UI widget that shows up multiple times in a view.
Does this mean you have to make the view programmatically? Is it time to break out the setFrame
and the addSubview
and the setTextColor
and the addTarget:action:forControlEvents:
?
Nope! You can still design your views in Interface Builder. Here’s how.
A reusable UITableViewCell
Want to create table view cells, but don’t want to use any of the predefined UITableViewCell styles? You can define your own table view cell style in a nib and use it to instantiate each cell in your table view.
This trick is straight out of Apple’s documentation. Open up your Apple textbook to the chapter called A Closer Look at Table-View Cells and turn to the section on “Loading Custom Table-View Cells From Nib Files”. Just follow the instructions under “The Technique for Dynamic Row Content” — or read on to skip to the punchline.
The following code is from my ReusableTableViewCellExample project. The crux of the UITableViewCell
trick lies in the implementation of the table view’s data source:
- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"ReusableTableViewCell"; ReusableTableViewCell *cell = (ReusableTableViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { // Create a ReusableTableViewCell from the nib and bind it to self.tableViewCell [[NSBundle mainBundle] loadNibNamed:@"ReusableTableViewCell" owner:self options:nil]; cell = self.tableViewCell; } // ... }
Why does this work?
ReusableTableViewCellExampleViewController
hasIBOutlet ReusableTableViewCell *tableViewCell
.- In ReusableTableViewCell.xib, File’s Owner is
ReusableTableViewCellExampleViewController
. - In ReusableTableViewCell.xib, File’s Owner has a connection from its
tableViewCell
outlet to theReusableTableViewCell
. [[NSBundle mainBundle] loadNibNamed:@"ReusableTableViewCell" owner:self options:nil]
creates an instance ofReusableTableViewCell
from ReusableTableViewCell.xib and assigns it toReusableTableViewCellExampleViewController
’stableViewCell
.
A reusable AQGridViewCell
You can pull basically the same trick with AQGridViewCell
as with UITableViewCell
, though it’s a bit… trickier.
In case you’re not familiar with AQGridViewCell
, it’s part of the very helpful AQGridView library, which allows you to display cells in a grid. AQGridView
is designed to be analogous to UITableView
.
When I wanted to create a reusable AQGridViewCell
, the first thing I tried was to copy the UITableViewExample
. But that didn’t work — my cells would either be wrongly laid out or just blank. Here’s the broken code:
// This code does not work! ReusableGridViewCell *cell = (ReusableGridViewCell *)[gridView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { [[NSBundle mainBundle] loadNibNamed:@"ReusableGridViewCell" owner:self options:nil]; cell = self.gridViewCellContent; }
To load a custom AQGridViewCell
from nib, here’s what worked for me. Instead of subclassing AQGridViewCell
, subclass UIView
. And instead of assigning the ReusableGridViewCell
loaded from the nib directly to cell
, add it as a subview of cell.contentView
. Here’s the working code:
AQGridViewCell *cell = (AQGridViewCell *)[gridView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { [[NSBundle mainBundle] loadNibNamed:@"ReusableGridViewCell" owner:self options:nil]; cell = [[[AQGridViewCell alloc] initWithFrame:gridViewCellContent.frame reuseIdentifier:CellIdentifier] autorelease]; [cell.contentView addSubview:gridViewCellContent]; }
So now that we’ve gotten the ReusableGridViewCell
into the AQGridViewCell
, we have to get it back out again to set its label and image according to its position in the grid. The way I chose to do that is with tags. Each UIView
can be assigned a tag — in Interface Builder the tag field is in the Attributes tab of the Inspector. A tag is just an integer. UIView
has a method called -viewWithTag:
that searches the view and all its subviews for the view with the given tag. So in ReusableGridViewCell.xib I set the tag of the top-level UIView
to 1, and in ReusableGridViewCellExampleViewController.m I search cell.contentView
for the view with that tag.
ReusableGridViewCell *content = (ReusableGridViewCell *)[cell.contentView viewWithTag:1];
The full example is in the ReusableGridViewCellExample project.
A reusable standalone view
In the previous examples, we created a reusable view by subclassing UITableViewCell
and AQGridViewCell
, which are both subclasses of UIView
. If you want to define a reusable standalone view (one that won’t be a subview of another view), you’re probably better off subclassing UIViewController
than UIView
. Why?
- A
UIViewController
subclass can be loaded from a nib with-initWithNibName:bundle:
. - Other view controllers can display your
UIViewController
subclass using methods like-[UIViewController presentModalViewController:animated:]
and-[UINavigationController pushViewController:animated:]
. - A
UIViewController
subclass can perform tasks at different points in the view lifecycle by overriding-viewDidLoad:
,-viewDidUnload:
,-viewWillAppear
, and so on.
The ReusableDatePickerViewExample project is an example of that. The goal is to create a view containing some controls (a date picker, a Done button, and a Cancel button), and be able to easily instantiate that view anywhere in the code. The controls are laid out in a nib. The UIViewController
subclass is called ReusableDatePickerViewController
.
iOS developers subclass UIViewController
all the time, so writing ReusableDatePickerViewController
is a cinch. The only thing to note is that ReusableDatePickerViewController
is decoupled from its parent view controller. Any view controller can display a ReusableDatePickerViewController
— and get messages back from it when the Done and Cancel buttons are tapped. This is accomplished by using the delegate pattern.
The parent view controller (ReusableDatePickerViewExampleViewController
) implements the ReusableDatePickerDelegate
protocol. After the parent view controller instantiates the ReusableDatePickerViewController
, it sets itself to be the ReusableDatePickerViewController
’s delegate. When the Done or Cancel button is tapped, ReusableDatePickerViewController
calls the appropriate ReusableDatePickerDelegate
method on the delegate.
A reusable subview
In the final example, we’ll create a reusable UI widget — a date picker with an on/off button that allows the date to be ignored. A use case for this ignorable date picker is to be able to pick either a definite date or “never”. Kosada used a similar widget in our timeline drawing app, Timestream, to let the user pick both finite (2011.10.1 - 2011.11.1) and infinite (2011.1.1 - forever) time ranges.
If you’re laying out a view in Interface Builder, wouldn’t it be nice if you could drop in an instance of the ignorable date picker just like you can drop in a UIButton
or UISwitch
or any other built-in UIView
?
You can (sort of) — here’s how. The full example is in the IgnorableDatePickerViewExample project.
First of all, the code for the ignorable date picker goes in a subclass of UIView
, not UIViewController
. Since IgnorableDatePickerView
is a UIView
, Interface Builder will let you drag an instance of it from Library onto the view you’re designing. (You could add an IgnorableDatePickerViewController
class, but here it’s not necessary.)
When you add IgnorableDatePickerView
as a subview in some other view’s nib, the IgnorableDatePickerView
acts as a placeholder. Annoyingly, it shows up as a plain white view, and you have to manually drag it to the right size. But hey, it beats having to lay out part or all of the parent view programmatically just because it has a custom subview.
In IgnorableDatePickerView.xib, you can lay out subviews and draw connections in IgnorableDatePickerView.xib in the usual way. There’s a trick to get the IgnorableDatePickerView
to load itself from IgnorableDatePickerView.xib. In IgnorableDatePickerView.xib, File’s Owner is IgnorableDatePickerView
and there’s a connection from IgnorableDatePickerView
’s contentView
outlet to the top-level view in the nib. IgnorableDatePickerView
overrides -[UIView awakeFromNib]
like so:
- (void) awakeFromNib { // Create a UIView from the nib and bind it to self.contentView [[NSBundle mainBundle] loadNibNamed:@"IgnorableDatePickerView" owner:self options:nil]; [self addSubview:self.contentView]; }
So, technically, the view defined in IgnorableDatePickerView.xib is not the IgnorableDatePickerView
itself, but its one and only subview. Notice the resemblance to the reusable table view cell example above.
IgnorableDatePickerView
could use the delegate pattern, just like ReusableDatePickerViewController
did, to notify its superview’s controller when its date picker is spun or its switch is toggled. But in this case I chose to make IgnorableDatePickerView
a passive widget. The superview’s controller is responsible for querying the IgnorableDatePickerView
for its state (by calling -[IgnorableDatePickerView dateString]
) whenever it wants the information.
By the way, the IgnorableDatePickerViewExample project uses the same trick of loading table view cells from nibs that I described in a previous post.
Summary
UITableViewCell
s, AQGridViewCell
s, standalone views, and subviews can all be designed for reuse. And you can still do the UI layout in Interface Builder. What a timesaver.
Jaymie Strecker is a software developer at Kosada, Inc. and one of the creators of Vuo.