tags:

views:

7773

answers:

8

The question is simple: How do you load custom UITableViewCells from Xib files? Doing so allows you to use Interface Builder to design your cells. The answer apparently is not simple due to memory managment issues. This thread mentions the issue and suggests a solution, but is pre NDA-release and lacks code. Here's a long thread that discusses the issue without providing a definitive answer.

Here's some code I've used:

static NSString *CellIdentifier = @"MyCellIdentifier";

MyCell *cell = (MyCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
    NSArray *nib = [[NSBundle mainBundle] loadNibNamed:CellIdentifier owner:self options:nil];
    cell = (MyCell *)[nib objectAtIndex:0];
}

To use this code, create MyCell.m/.h, a new subclass of UITableViewCell and add IBOutlets for the components you want. Then create a new "Empty XIB" file. Open the Xib file in IB, add a UITableViewCell object, set its identifier to "MyCellIdentifier", and set its class to MyCell and add your components. Finally, connect the IBOutlets to the components. Note that we did not set the File's Owner in IB.

Other methods advocate setting the File's Owner and warn of memory leaks if the Xib is not loaded via an additional factory class. I tested the above under Instruments/Leaks and saw no memory leaks.

So what's the canonical way to load cells from Xibs? Do we set File's Owner? Do we need a factory? If so, what's the code for the factory look like? If there are multiple solutions, let's clarify the pros and cons of each of them...

+1  A: 

I dont know if there is a canonical way, but here's my method:

  • Create a xib for a ViewController
  • Set the File Owner class to UIViewController
  • Delete the view and add an UITableViewCell
  • Set the Class of your UITableViewCell to your custom class
  • Set the Identifier of your UITableViewCell
  • Set the outlet of your view controller view to your UITableViewCell

And use this code:

MyCustomViewCell *cell = (MyCustomViewCell *)[_tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
  UIViewController* c = [[UIViewController alloc] initWithNibName:CellIdentifier bundle:nil];
  cell = (MyCustomViewCell *)c.view;
  [c release];
}

In your example, using

[nib objectAtIndex:0]

may break if Apple changes the order of items in the xib.

Stephan Burlot
+5  A: 

Hi,

I have done this a bunch of different ways. I just posted on my blog my current favorite.

Bill Dudney
+2  A: 

Here's the class method that I've been using for creating custom cells out of XIBs:

+ (CustomCell*) createNewCustomCellFromNib {
        NSArray* nibContents = [[NSBundle mainBundle]
                                                        loadNibNamed:@"CustomCell" owner:self options:NULL];
        NSEnumerator *nibEnumerator = [nibContents objectEnumerator];
        CustomCell *customCell= nil;
        NSObject* nibItem = nil;
        while ( (nibItem = [nibEnumerator nextObject]) != nil) {
                if ( [nibItem isKindOfClass: [CustomCell class]]) {
                        customCell = (CustomCell*) nibItem;
                        if ([customCell.reuseIdentifier isEqualToString: @"CustomCell"]) {
                              break; // we have a winner
                        }
                        else
                                fuelEntryCell = nil;
                }
        }
        return customCell;
}

Then, in the XIB, I set the class name, and reuse identifier. After that, I can just call that method in my view controller instead of the [[UITableViewCell] alloc] initWithFrame:]. It's plenty fast enough, and being used in two of my shipping applications. It's more reliable than calling [nib objectAtIndex:0], and in my mind at least, more reliable than Stephan Burlot's example because you're guaranteed to only grab a view out of a XIB that is the right type.

Shawn Craver
+5  A: 

Loading UITableViewCells from XIBs saves a lot of code, but usually results in horrible scrolling speed (actually, it's not the XIB but the excessive use of UIViews that cause this).

I suggest you take a look at this: http://blog.atebits.com/2008/12/fast-scrolling-in-tweetie-with-uitableview/

Can Berk Güder
+2  A: 

What I do for this is declare a IBOutlet UITableViewCell *cell in your controller class. Then invoke the NSBundle loadNibNamed class method, which will feed the UITableViewCell to the cell declared above.

For the xib I will create an empty xib and add the UITableViewCell object in IB where it can be setup as needed. This view is then connected to the cell IBOutlet in the controller class.

- (UITableViewCell *)tableView:(UITableView *)table
         cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

NSLog(@"%@ loading RTEditableCell.xib", [self description] );

static NSString *MyIdentifier = @"editableCellIdentifier";  
cell = [table dequeueReusableCellWithIdentifier:MyIdentifier];

if(cell == nil) {
    [[NSBundle mainBundle] loadNibNamed:@"RTEditableCell" 
                                  owner:self 
                                options:nil];
}

return cell;

}

NSBundle additions loadNibNamed (ADC login)

cocoawithlove.com article I sourced the concept from (get the phone numbers sample app)

Ryan Townshend
So how does the memory management of this approach work? There's only one pointer to the XIB-loaded cell, the controller's IBOutlet. As new cells are needed, new XIBs are loaded. Won't that leave an extra retain on the cell object when the IBOutlet is reassigned? Or is it a property with (retain)?
DrGary
+11  A: 

Here is two methods which the original author states was recommended by an IB engineer.

See the actual post for more details. I prefer method #2 as it seems simpler.

Method #1:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BDCustomCell"];
    if (cell == nil) {
  // Create a temporary UIViewController to instantiate the custom cell.
        UIViewController *temporaryController = [[UIViewController alloc] initWithNibName:@"BDCustomCell" bundle:nil];
  // Grab a pointer to the custom cell.
        cell = (BDCustomCell *)temporaryController.view;
  // Release the temporary UIViewController.
        [temporaryController release];
    }

    return cell;
}

Method #2:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BDCustomCell"];
    if (cell == nil) {
  // Load the top-level objects from the custom cell XIB.
        NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:@"BDCustomCell" owner:self options:nil];
  // Grab a pointer to the first object (presumably the custom cell, as that's all the XIB should contain).
  cell = [topLevelObjects objectAtIndex:0];
    }

    return cell;
}
bentford
Good link; it also discusses memory managment issues. Thanks!
DrGary
Nice solutions. This lets you reuse cells with different owners, something most solutions don't let you do because they bind outlets from a known-owner class.
Mike Weller
A: 

If you're using Interface Builder to make cells, check that you've set the Identifier in the Inspector. Then check that it's the same when calling dequeueReusableCellWithIdentifier.

I accidentally forgot to set some identifiers in a table-heavy project, and the performance change was like night and day.

alex
+4  A: 

Took Shawn Craver's answer and cleaned it up a bit.

BBCell.h:

#import <UIKit/UIKit.h>

@interface BBCell : UITableViewCell {
}

+ (BBCell *)cellFromNibNamed:(NSString *)nibName;

@end

BBCell.m:

#import "BBCell.h"

@implementation BBCell

+ (BBCell *)cellFromNibNamed:(NSString *)nibName {
    NSArray *nibContents = [[NSBundle mainBundle] loadNibNamed:nibName owner:self options:NULL];
    NSEnumerator *nibEnumerator = [nibContents objectEnumerator];
    BBCell *customCell = nil;
    NSObject* nibItem = nil;
    while ((nibItem = [nibEnumerator nextObject]) != nil) {
        if ([nibItem isKindOfClass:[BBCell class]]) {
            customCell = (BBCell *)nibItem;
            break; // we have a winner
        }
    }
    return customCell;
}

@end

I make all my UITableViewCell's subclasses of BBCell, and then replace the standard

cell = [[[BBDetailCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"BBDetailCell"] autorelease];

with:

cell = (BBDetailCell *)[BBDetailCell cellFromNibNamed:@"BBDetailCell"];
vilcsak