Reusable Views in iOS Reusable Views in iOS

Posted by jstrecker on 2011.10.29 @ 11:28

Filed under:

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 has IBOutlet ReusableTableViewCell *tableViewCell.
  • In ReusableTableViewCell.xib, File’s Owner is ReusableTableViewCellExampleViewController.
  • In ReusableTableViewCell.xib, File’s Owner has a connection from its tableViewCell outlet to the ReusableTableViewCell.
  • [[NSBundle mainBundle] loadNibNamed:@"ReusableTableViewCell" owner:self options:nil] creates an instance of ReusableTableViewCell from ReusableTableViewCell.xib and assigns it to ReusableTableViewCellExampleViewController’s tableViewCell.

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

UITableViewCells, AQGridViewCells, standalone views, and subviews can all be designed for reuse. And you can still do the UI layout in Interface Builder. What a timesaver.

AttachmentSize
ReusableGridViewCellExample.zip127.71 KB
ReusableDatePickerViewExample.zip119.02 KB
IgnorableDatePickerViewExample.zip34.58 KB
ReusableTableViewCellExample.zip38.57 KB

I am having a hard time understanding why this works. In ReusableGridViewCellExampleViewController.h there is an outlet “gridViewCellContent”, but it doesn’t seem to be hooked up (and there is nothing available to hook it up to…) Yet it works correctly. Could you point me in the right direction to why this works?

ReusableGridViewCellExampleViewController (oh man, sorry for the ridiculous name) is the File’s Owner in two xibs — ReusableGridViewCellExampleViewController.xib and ReusableGridViewCell.xib. It’s in ReusableGridViewCell.xib that gridViewCellContent is hooked up to File’s Owner. ReusableGridViewCell.xib gets loaded in -gridView:cellForItemAtIndex:.

I see that you have added those “reusable” views in your .xib file. Is there anyway to add them programmatically? As in according to user input i want to show widgets 1,2 and 3 or 3,4 and 5 accordingly?

Sure. If you wanted to instantiate IgnorableDatePickerView programmatically instead of in the xib, you would move the code in its -awakeFromNib method over to -initWithFrame.

This was a big help for me. There were lots of StackOverflow threads on this topic, but you providing the Project file download helped me figure out what I needed to do.Thanks!