7

I'm reading a custom table cell in tableView:cellForRowAtIndexPath: from a nib file. This works great for my purposes, except it's quite slow.

Now, I know the right thing to do in the long term is to create the cell entirely in code, and to use a single view, and so on. But this is a prototype, and I don't want to put that much effort into it.

For now, I'd be happy if I was reading the nib only once in the UIViewController subclass, then tableView:cellForRowAtIndexPath: made copies of it. My assumption here is that copying would be faster than reading the nib.

Here's what I use to load the nib, which I call from viewDidLoad: (and retain after)

-(id)loadFromNamed:(NSString*)name {
    NSArray *objectsInNib = [[NSBundle mainBundle] loadNibNamed:name
                                                          owner:self
                                                        options:nil];
    assert( objectsInNib.count == 1 );
    return [objectsInNib objectAtIndex:0];
}

All is good so far. But the question is: How do I copy this over and over? Is it even possible?

I tried [_cachedObject copy] and [_cachedObject mutableCopy] but UITableViewCell doesn't support either copy protocol.

If I have to, I can just tell them to ignore the speed until I'm prepared to remove the nib entirely, but I'd rather get it going a little faster if there's a low-hanging fruit here.

Any ideas?

iDeveloper
  • 940
  • 1
  • 10
  • 45
Steven Fisher
  • 44,462
  • 20
  • 138
  • 192

5 Answers5

8

I think coping of table cell can be used together with dequeuing mechanism, which will allow to create cell one time (from nib or programmatically or getting it loaded automatically from other nib and linking as an outlet in IB) and then clone it or dequeue it when needed.

UITableViewCell doesn't conform to NSCopying protocol, but it supports keyed archiving/unarchiving mechanism, so it can be used for cloning.

Based on answer " How to duplicate a UIButton in Objective C? " my data source delegate method looks like:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellID = @"CellIdentifier";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellID];

    if (!cell) {
        NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject:self.tableViewCell];
        cell = [NSKeyedUnarchiver unarchiveObjectWithData:archivedData];
    }

    // ... config ...

    return cell;
}

And in my case self.tableViewCell is a cell that was loaded one time from view's nib file.

I don't tested what will be faster: "archive + unarchive" to clone or "load nib file + unarchive" which framework will do in case of -loadNibNamed:owner:options:, I used this method only with convenience considerations, but good chances that memory operation vs file operation will be faster.

EDIT: It appears not as easy as it seemed at first. As UIImage doesn't conforms to NSCoding, cells with configured UIImageViews can't be just copied without additional code. Yep, copying whole image is definitely not a good practice, cheers to Apple for pointing this.

Community
  • 1
  • 1
zubko
  • 1,718
  • 25
  • 28
6

Use the cell cloning built into the table view. Apple knew generating a lot of table cells was slow. Check out the docs for this method:

- (UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier

You create the cell once, then as new cells are requested, use that method to clone the existing cells. Then you just change what needs to be changed about the new cell and return the cell object.

Also check out the table view realted sample code provided by Apple that uses this method and show you the right way. The fact your cell was loaded from a nib shouldn't matter at all.


Minor clarification: I dont think the above method clone cells for you. Instead it takes cell object that have scrolled off the screen and simply moves them to a new spot. So it's literally reusing a cell. So be sure your custom table view can be set to all the new values it needs outside of the intialization.

Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
  • Thank you. I had forgotten about `dequeueReusableCellWithIdentifier`. Now I'm using it, but it's still slower than I'd like. That suggests that the bottleneck is in drawing, which will have to wait until I have time to apply the other fast table tricks. But now I know what's going on! – Steven Fisher Feb 20 '09 at 21:20
  • Well your approach would have been even slower since reusing objects is far quicker than cloning objects. Glad I could help. – Alex Wayne Feb 20 '09 at 23:01
  • Try profiling on the device to see for certain what is running slow. Some people say the trick is to use only one view per cell and do all your own drawing. – Chris Lundie Feb 20 '09 at 23:12
  • 6
    This is not actually the correct answer to this question. On initial setup dequeuing will return nil for each visible cell. So in that `if (cell == nil)` block you will want to create a new UITableViewCell, probably by loading from the nib. I would also prefer to copy the UITableViewCell from an IBOutlet in my controller which I've wired up to the designed UITableViewCell in the nib. But copy fails, saying it doesn't support copyWithZone. – dlamblin Jun 16 '09 at 13:53
  • dlamblin, you're in exactly the situation that inspired my question. But I accepted this as the correct answer because I realized that if I'm using the cache, the most times I'll be running through loadNibNamed is once per initially visible row. Say, 5 times. I just don't need to optimize that; the real bottleneck has to be in the drawing, or in my repurposing the cell for different rows later in the table. (Haven't solved this yet, as I've been on another project.) – Steven Fisher Jun 16 '09 at 17:23
  • 2
    Is there a way to create a prototype instance of `UITableViewCell` in Interface Builder and just clone that as many as needed by the `UITableView` ? It's much easier than setting up the cell by code. Thanks. – adib Apr 16 '10 at 09:11
  • 1
    @adib There is a class is SDK 4.0 that helps with this called `UINib`. But not an easy to do it in the currently available OS. – Alex Wayne Apr 16 '10 at 16:05
  • For posterity, this is now easy with iOS 5 Storyboards. – Steven Fisher Aug 08 '12 at 15:39
4

Not proud of this solution, but it works with the maximum number of possible IB bindings:

Interface (AlbumTableViewCell is a subclass of UITableViewCell of which an instance is defined in AlbumViewController's XIB file):

@interface AlbumsViewController : UITableViewController {
    IBOutlet AlbumTableViewCell *tableViewCellTrack;
}

@property (nonatomic, retain) AlbumTableViewCell *tableViewCellTrack;

Implementation (unarchive / archive makes a copy / clones the table view cell):

@implementation AlbumsViewController

@synthesize tableViewCellTrack;

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    AlbumTableViewCell *cell = (AlbumTableViewCell *)[tableView dequeueReusableCellWithIdentifier: @"AlbumCell"];

    if (cell == nil) {
        AlbumsViewController *albumsViewController = [[[AlbumsViewController alloc] init] autorelease];
        [[NSBundle mainBundle] loadNibNamed: @"AlbumsViewController" owner: albumsViewController options: nil];

        cell = albumsViewController.tableViewCellTrack;
    }

    cell.labelTitle.text = ...;
    cell.labelArtist.text = ...;

    return cell;
}
pfo
  • 334
  • 4
  • 5
3

Well, I'm not sure why all the tutorials out there doesn't specify this step.

When using your own custom UITableViewCell from Nib, calling dequeueReusableCellWithIdentifier is not enough. You have to specify the "Identifier" in the IB, just for for it in the Table View Cell tab section.

Then make sure the identifier you put in IB is the same as the identifier you use for the dequeueReusableCellWithIdentifier.

Teo Choong Ping
  • 12,512
  • 18
  • 64
  • 91
1

Here it is in Swift

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    var cell : UITableViewCell?
    let cellId = String(format: "Cell%d", indexPath.row)
    cell = alertTable!.dequeueReusableCellWithIdentifier(cellId) as! UITableViewCell?

    if cell == nil {
        let archivedData = NSKeyedArchiver.archivedDataWithRootObject(masterTableCell!)
        cell = NSKeyedUnarchiver.unarchiveObjectWithData(archivedData) as! UITableViewCell?
    }

    // do some stuff

    return cell!
}
Joe C
  • 2,728
  • 4
  • 30
  • 38