Tuesday, September 8, 2009

Table View Cells in Interface Builder - the Apple Way™

When Dave and I originally wrote Beginning iPhone Development, the SDK was very much in beta, and a lot of the documentation was incomplete or completely lacking. Given that, I think we did a pretty good job trying to stick with Apple's "best practices" and at guessing at which techniques would later evolve into best practices. In a surprising number of places, we do it the Apple Way™, even though we weren't 100% sure what the Apple Way™ would be when we were writing the book.

We did well, but we didn't bat 1.000.

One place where we missed was in loading table view cells created in Interface Builder.

In the first edition of the book, we hardcoded the index of the cell. That was bad, because when 2.1 shipped, the structure of the array returned by NSBundle changed and our code broke. Whoops!

In the second edition, we changed it so that the code no longer uses a hard-coded index, but instead iterates over the returned array looking for an object of the right class (UITableViewCell). That was a much better approach and works fine.

Somebody at Apple thought of a better way. It's effing brilliant. So brilliant, in fact, that when I first encountered the documentation for it, it took me a while to figure out just what they were doing. Here's the example code from Apple's documentation:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = @"MyIdentifier";

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
[[NSBundle mainBundle] loadNibNamed:@"TVCell" owner:self options:nil];
cell = tvCell;
self.tvCell = nil;
}

UILabel *label;
label = (UILabel *)[cell viewWithTag:1];
label.text = [NSString stringWithFormat:@"%d", indexPath.row];

label = (UILabel *)[cell viewWithTag:2];
label.text = [NSString stringWithFormat:@"%d", NUMBER_OF_ROWS - indexPath.row];

return cell;
}

This is what confused me at first:

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

The method loadNibNamed:owner:options returns an NSArray with the contents of the nib, but this code completely ignores that array. It doesn't even capture it. Then, it goes on and blithely assigns some instance variable to another instance variable, and then sets the first instance variable to nil.

Apple's documentation isn't perfect, but usually, it's pretty damn good, so I was pretty sure this code was right, I just wasn't getting something. There was a piece of the puzzle missing.

So, I looked at tvCell, which was declared earlier, and saw that it was an IBOutlet.

“Curiouser and curiouser!” Cried Alice.

Then it dawned on me. The bundle loader automatically connects outlets made to File's Owner when it loads a nib file if the outlet is nil. Notice that when the nib is loaded, the code specifies self as the bundle's owner. So, since this controller class is the File's Owner, when we load the nib, the outlet will get connected to an object in the nib if the outlet with that name on File's Owner is connected to an object in the nib.

So, what's happening is, the cell is created in its own nib file. The File's Owner for that nib file is our table view controller where the code above resides. In the nib file, the tvCell outlet on File's Owner is connected to the custom cell that gets created.

So, after this line of code:
[[NSBundle mainBundle] loadNibNamed:@"TVCell" owner:self options:nil];

tvCellis automatically connected to the custom cell that was created by virtue of the outlet connection in the nib. There's no need to loop or worry about indices. The bundle loader just does it automatically. So, that custom cell is then assigned to cell which is the table view cell that this method returns to the table to display the contents of this row. After that, tvCell is set to nil so that the next time this method gets called, the bundle loader once again connects the outlet for us.

That's just brilliant. Kudos to whomever at Apple thought of it.

If you'd like to see it in action, here's an Xcode project that uses this technique.



29 comments:

FallOutBoy said...

Thanks for clarifying this. That was something I didn't quite get before.

Now all makes sense.

Malcolm Hall said...

I think that code is pretty hacky because of the IBOutlet ivar that is used like a temporary accessor to the cell, and the confusing use of loadNibNamed. The use of viewWithTag instead of a table view subclass using IBOutlets and properties is a pretty clear indication this is a hacky coder.

If you make a nib with a custom table cell and set the file's owner to be UIViewController and the view to the cell then you can take advantage of the nib loading feature of that class:

static NSString *MyIdentifier = @"ScanCell";

ScanTableCell *cell = (ScanTableCell *)[tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
UIViewController* c = [[UIViewController alloc] initWithNibName:@"ScanTableCell" bundle:nil];
cell = (ScanTableCell *)c.view;
[c release];

Ihtsl9INn8QzZomfXm9M1Oj4AAFMhtc5fdYIXZmgCw-- said...

I was just going to do this in a project that I'm working on yesterday, but I thought for sure that something would be hosed up involving two file owners, etc. I'm doing it outside of a table view, but it's the same principle. Time to go back and see if it works in my instance!

pippin said...

That's not brilliant, that's coding by side effects. Go straight to debug hell, boy.
If you want code that is really, really hard to maintain and where you will end up having bugs you never ever find, this is the way to go.
I for myself will stay away from things like this.

Somewhat reminds me of Byte's (is Byte still around, btw?) old "program in a single line of C-Code" contest; you can be proud of mastering insane complexity but never ever do this for a real world project.

fpillet said...

Additionally to what has been said before, you mentioned that "The bundle loader automatically connects outlets made to File's Owner when it loads a nib file if the outlet is nil".

AFAIK that is incorrect, the bundle loader uses a variety of documented techniques to try and connect the outlet, but does not check whether the outlet value is nil.

bbrown said...

The Apple documentation never used to have that method. I know because I combed through it trying to use IB for UITableViewCells. Eventually, I ended up with something almost exactly like what you're describing here and it arose from gkstuart's helpful post on the discussion forum. I wouldn't be surprised if that was the genesis of the Apple example.

Jeff LaMarche said...

fpillet:

It may not be documented behavior, but I can affirm that that is the actual behavior. If you leave the outlet connected to the old cell, it will fail to connect it the next time this method calls.

It's easy enough to test, and I have.

Malcolm:

Saying it's hacky makes the assumption that there's some rule that an IBOutlet has to point to the same object throughout the controller's lifetime. IBOutlets are just pointers and there's no reason they have to always point to the same thing. Don't assume an uncommon use case is wrong simply because it's uncommon or new.

Your example is a great solution if you have a situation where your custom cell needs its own controller. In other situation, it's just a more costly way to do the same thing as Apple's code because you're creating objects you don't need.

Pippin:

Which part, exactly, is the "side effect". This uses documented behavior to enable you to write less code. In my mind, this is fully keeping with Interface Builder's mantra of "any code you don't write is code you don't have to debug or maintain". The bundle loader code is far better tested than anything you or I have written, having been around since the late eighties and used in thousands of applications.

Now, I don't particularly fear debugging, so "debugging hell" for me is a rare and special place I only get to from really gnarly bugs. That said, I have used this approach several times with some relatively complex cells and have had no issues whatsoever. It's no harder to debug than any other nib, and no more hacky.

Your "bugs you will never find" statement is just inaccurate (I always find them :P ), and if it weren't, it would mean that using Interface Builder anywhere is a bad idea. If you're somebody who believes that, then there's no chance of us finding common ground here and will have to agree to disagree, because I'm a strong believer in the value of IB.

Ihtsl9INn8QzZomfXm9M1Oj4AAFMhtc5fdYIXZmgCw--

There's absolutely no problem with an object being the File's Owner for more than one nib. It can't go the other way (one nib having multiple File's Owners, but this is completely fine.

Everyone:

I, for one, really value creativity and novel (but intelligent) uses of documented functionality. I don't see this as hacky at all. I see this as someone looking at the available functionality and coming up with a new way to use it that results in less code to write and maintain. It leverages existing, well-tested code to get the job done. That's not hacky, that's creative.

The ability to find new and better ways to solve a problem is the difference a code monkey and a true developer.

Don't fear trying new and different things (but do learn when to give up on an idea that doesn't work). Somebody's always got to be the first one to do something, and there will always be people who criticize novel approaches simply because they are novel.

If you don't want to use this approach, by all means, go write more code. I've seen a lot of code in my day, and this impresses me as being novel, useful, and elegant.

Feel free to disagree, though. Everybody's allowed to be wrong sometimes. :)

alanquatermain said...

The only thing in that code that I don't particularly like the look of is the assumption that the loaded cell object will stay in memory nicely after sending 'self.tvCell = nil;'. I don't see any code there to ensure this happens, so there's an assumption happening there. And we know where we get bitten by assumptions don't we?

I'd change the second line to:

cell = [[tvCell retain] autorelease];

That guarantees that the memory contract works as expected— that the function returns an object with a single autoreleased reference count. It does that, and any other references are the sole responsibility of NSBundle.

pippin said...

Jeff,

the side effect is that you can't see in the code what's happening. You violate your class' abstraction by having an internal function of UITableViewCell (which your nib is) manipulate a UITableViewController variable without using a defined interface. Or did I not get something here?

Re "The ability to find new and better ways to solve a problem is the difference a code monkey and a true developer."

Disagree. The difference between a code monkey and a true developer is the ability to design things before starting to code.

Jeff LaMarche said...

Jim;

That's a good point, although it does seem to work as it exists in Apple's documentation. I'm guessing that the bundle loader either has retained the objects, or has already put them into the autorelease pool

But I like your change. It's safer because it doesn't rely on something else to keep it from getting deallocated and doesn't add much code.

Alex said...

alanquatermain:
Top level objects in nibs are autoreleased in the iPhone, setting 'self.tvCell = nil;' won't change that fact, and since the outlet is defined as "assign" the cell will be valid for the rest of the method, and be retained by the tableview, so it's all good.

I've been using this technique for a while now, and I agree with Jeff, it's nice and clever, just think about the way it works for a second or two.

alanquatermain said...

I realize that the code is safe— I just believe that one should always make things like memory management explicit. So even if you're returning something like this which is ultimately owned by an autoreleased array, you should still codify it directly by doing your own retain/autorelease. That mitigates two potential issues:

1. The rules change in a future software release (e.g. NSBundle stops including IBOutlet vars in the autoreleased array).

2. Someone who is unfamiliar with the rules can still see exactly what is happening, because your explicit retain/autorelease codifies that at the point of origin for all to see.

Jeff LaMarche said...

The side effect is that you can't see in the code what's happening.

You can see what's happening if you understand the way NSBundle and IB work. The fact that the approach is not obvious doesn't mean you can't see it.

You violate your class' abstraction by having an internal function of UITableViewCell (which your nib is) manipulate a UITableViewController variable without using a defined interface.

I don't see where the abstraction is violated. I'm sure you have a point here, but I'm not grokking it. The controller has its own view, which is a 1:1, and then it has a one to many relationship to the nib with the table view cell.

We're not having "an internal" of UITableViewCell manipulate anything. We're using the bundle loader, and its doing its job, which is to load a nib file and connect the outlets and actions defined in that nib file. Besides, the variable being manipulated doesn't belong to UITableViewController, it belongs to our subclass and the connection is happening because of actions our subclass takes, so I really don't see the problem with the abstraction. You might very well have a point, but if you do, I'm just not seeing it.

Disagree. The difference between a code monkey and a true developer is the ability to design things before starting to code.

I definitely disagree with this. I'd actually say the real difference is that a real developer doesn't need a complete design before they start coding. The idea of design and code as two separate, discrete steps gives horrible results. I've worked in environments where that is the norm, and it almost always gives a bad result. The business world seems to refuse to move on from the Structured Analysis methodology, no matter how many times they fail using it.

But, in this environment is where code monkeys excel. They are given a design, and they write to it, literally, and exactly as written, flaws and all. Years ago, when I had to work in that environment, I did well (and kept my sanity) only by breaking the rules. I would write the application, then write the design documents after the fact.

There's almost nobody on the planet who can design a complex application completely before starting to write code. It's nearly impossible to anticipate everything in a "design". Many of the nuances and difficulties inherent in complex software simply don't become apparent until some code has been written.

For me, the extent of design before coding is some screen drawings and a high-level diagram of the architecture, then I immediately start prototyping. The trick is to never get so attached to something that you won't throw it out if it's not right.

By your definition, I'm a code monkey. Perhaps I am, but I'm getting pretty decent results by always breaking your rule.

pippin said...

Ok, I'll have to have a closer look at what the ib loader does, to me it still feels bad to change something in a class the nib doesn't belong to.

Re coding. "I'd actually say the real difference is that a real developer doesn't need a complete design before they start coding."

I didn't say "complete design". But I insists that it is NOT a waste of time to think about what you are trying to do before you start coding.

I admit that it depends on a certain degree on the scale of the project, but right now it's my everyday annoyance to cope with the "let's code first and think later" attitude. You can get away with that as long as you work along but today there are few projects where you do that.
Prototyping is fine but then you have to scrap your prototype before you go with the final design. I can show you countless projects where developing the prototype into a real product lead to a mess.
You change things on the left hand and all of a sudden something else breaks in someone else's code on the right because you never agreed on what you are actually doing and then you add another change which breaks something else.

Sorry, been there, seen that a lot. I stick to it: leaning back and turning the brain on before getting to code is the difference between hacking (which can work fine as long as you work alone) and development, which is essential as soon as several people are involved or even outside parties. I know it's boring and I know it's against the "genius" in us all but there is a scale where it starts to pay off and that scale is NOT an enterprise application but it starts much much earlier.

Jack Nutting said...

Great tip, Jeff! This technique is absolutely The Right Way to load a tableviewcell from a nib file. When I first saw the "accepted" algorithm of iterating through the result returned by loadNibNamed:owner:options:, looking for the right object, I nearly wept at the contortion. I've been doing Cocoa programming forever, where the similar method loadNibNamed:owner: just returns a BOOL, so you can check for a total nib-loading failure, so the whole idea of returning all the top-level objects strikes me as bizarre. Let the frozen object graph be a frozen object graph, I say!

One of the main points of loadNibNamed:owner:options: is that it lets you insert the caller (or another object) right into the object graph as the File's Owner, and will establish all connections accordingly. The fact that loadNibNamed:owner:options: manipulates instance variables is no more a "side effect" than when any other method manipulates instance variables.

Daniel Tull said...

I've not made my mind up about whether I really like this approach or not. However, I'm not sure why they didn't use two more IBOutlets for the two labels. Can't see why you would mix the two methods or getting to the object (via tags or outlets) like this, just seems very inconsistent now.

Jack Nutting said...

I agree Daniel, it makes even more sense to take things to their logical conclusion and use outlets for all the views you need to get to, instead of referring to them by tag. IMHO.

Joe Conway said...

I'm not sure what test you are running to determine that a non-nil outlet doesn't get overwritten when loading a NIB. Every test I have done shows the opposite. It would make sense that a non-nil outlet gets set again:

1. UINibLoading uses key-value coding.
2. Being non-nil doesn't mean there is an object there, it could just be a dangling pointer.
3. Memory warnings would wreck havoc on a view hierarchy - a subview/outlet of a view controller would fall out of sync when a memory warning occurs.

You can test this fairly easily. This is especially important given the behavior low memory warnings (which is a great way to test it) - and yet another reason not to instantiate view controllers in a VC's XIB.

On Table View Cells, my question is why put them in a XIB? All you get is the layout, something you could certainly do in code. Reading from the disk to create table view cells each time you need a new cell seems horribly inefficient to me. Especially because there are going to be n separate reads, not just one for all of the cells needed.

And it does look hackish. Being clever is useful when you have no other options in a API, I find it misleading otherwise.

The real solution would be to have UITableViewCell conform to NSCopying. A table view controller would have outlets to a XIB-created cell, and it would create copies when a new cell of that type is needed.

Laurie said...

I really don't understand why you don't just use the instance variable throughout:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = @"MyIdentifier";

tvCell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (tvCell == nil)
[[NSBundle mainBundle] loadNibNamed:@"TVCell" owner:self options:nil];

UILabel *label;
label = (UILabel *)[tvCell viewWithTag:1];
label.text = [NSString stringWithFormat:@"%d", indexPath.row];

label = (UILabel *)[tvCell viewWithTag:2];
label.text = [NSString stringWithFormat:@"%d", NUMBER_OF_ROWS - indexPath.row];

return tvCell;
}

fpillet said...

Jeff, your personal tests may have shown that non-nil outlets don't get overwritten, but my use of nib loading show the opposite.

In one application I have a UIViewController that loads a second nib repeatedly to instantiate multiple copies of a particular view. In this case, nib loading connects the outlets to the new view's objects, releasing the previous ones. This works perfectly well.

Now there may be exceptions to this, I'll investigate various cases because I really depend on this in this particular application, and if the behavior changes accross OS releases I'll be in trouble. But I suspect, given the way this works (KVC assignment of outlets), especially when you declare the outlet as properties, that this won't change.

Corey said...

This is similar to Bill Dudney's code here:
http://bill.dudney.net/roller/objc/entry/uitableview_from_a_nib_file

But instead he uses a controller object for each cell, which saves him from reseting an outlet to nil everytime a cell is loaded.

zanzibar said...

I was really excited when I found this post. It seemed like a really elegant and easy solution to specify a UITableViewCell in a nib. Although I would agree it feels like a hack. So it worked right away for me however the story is different when you load up the example app in Insturments.

Looking at object allocations for TableCell while constantly scrolling up and down you see that the transitory and overall objects as well as overall bytes keep on increasing. If you just programatically create the cell they seem to stay at less than 10.

mdomans said...

Because one thing you do not do here is REUSING the cells. This is bad.

Daniel Tull said...

So ever since I posted the comment in September, it's been in the back of my mind that I wanted to make the automation of hooking outlets to table view cells in a nib much nicer.

This has lead to me thinking of a few different ways, that really didn't solve the issue. The problem for me is that while the code in the post is great, you still need to use the arbitrary tags on the views in IB. This seems to me like it could easily fail if they get changed, which wouldn't give warnings etc.

So I came up with a nice (imo) solution I've called DTNibHook. It can be found at http://bitbucket.org/danielctull/dtnibhook. Essentially, you subclass DTNibHook and add the properties you wish to link up in the nib. When it loads the nib it will go through each property and change the tag number to its internal count.

When you come to reuse a cell, you can give the nib hook the view (or cell in this case) and it will set the properties based on the value of the tags it previously assigned.

Assuming you use a nib hook with the same set of properties, it will link properly. Hopefully people find this helpful. A demo app shows how I'm using it for a table view.

Edwin said...

scrub m65 kamagra attorney lawyer body scrub field jacket lovegra marijuana attorney injury lawyer

Victor said...

Don't forget that the argument to dequeueReusableCellWithIdentifier: must be the same as the "Identifier" value defined in IB, in this case "MyIdentifier".

I had weird crashes because I missed that.

Victor said...

Don't forget that the argument to dequeueReusableCellWithIdentifier: must be the same as the "Identifier" value defined in IB, in this case "MyIdentifier".

I had weird crashes because I missed that.

antywong said...

The rounded shape of speedy 30 features textile fake louis vuitton lining and leather trimmings with shiny Louis Vuitton Monogram ldylle Romance Encre golden brass. Sized at 11.8" x 8.3" x 6.7", the large capacity Hermes Original Python Birkin 30 Grey of this bag is enough for handbags review daily essentials; you can put bags wholesale everything into this city bag. It also fits for Hermes Clemence Jypsiere 34 Purple every occasion and perfectly goes with any outfits mfakng100910.

h4ns said...

What youre saying is completely true. I know that everybody must say the same thing, but I just think that you put it in a way that everyone can understand. I also love the images you put in here. They fit so well with what youre trying to say. Im sure youll reach so many people with what youve got to say.

Arsenal vs Huddersfield Town live streaming
Arsenal vs Huddersfield Town live streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Notts County vs Manchester City Live Streaming
Notts County vs Manchester City Live Streaming
Bologna vs AS Roma Live Streaming
Bologna vs AS Roma Live Streaming
Juventus vs Udinese Live Streaming
Juventus vs Udinese Live Streaming
Napoli vs Sampdoria Live Streaming
Napoli vs Sampdoria Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
AS Monaco vs Marseille Live Streaming
AS Monaco vs Marseille Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Technology News | News Today | Live Streaming TV Channels