Showing posts with label UINib. Show all posts
Showing posts with label UINib. Show all posts

Tuesday, July 10, 2012

UITableView How-To: Part 5 - More About XIB-Based Cells

Part 1 | Part 2 | Part 3 | Part 4 | Part 6

 

(This is really the second half of Part 4, so we'll just continue with the same project)

 

I've got one more key topic to address, then we'll take a look at some alternate techniques.

 

By the end of Part 4, we created a custom view layout using IB, so we've got the visual part down. That really just leaves one more basic characteristic to talk about: actions. In addition to customizing the look of your cell, you can customize the behavior of your cell, and this is most easily done by adding controls such as buttons, switches, and so on. We're just going to do a quick button, but the concept applies to any of the other controls you may wish to use.

 

So, open up CustomTableCell.xib and drop a button smack in the middle:

 

 

With a typical view controller, we'd connect this button to an IBAction. That's what we want to do here as well, but the question is: where is the IBAction? There are 2 basic options, in the cell class or in the view controller class. Where you should put it will depend on the goal you are trying to accomplish. If you put it in the cell class, then it really isn't any different than wiring it up as you would in a view controller. Depending on your goal, this approach can either simplify or complicate your ultimate outcome. But for our purposes here, we're going to put the IBAction in the view controller. So add this declaration to BasicViewController.h:

 

- (IBAction)buttonPressed:(UIButton *)button;

 

(you are probably accustomed to seeing (id)sender for these things. Go ahead and do that if you prefer. I like the type to be correct, and rarely have a need to mix these methods with multiple types of controls)

 

Now we just need to wire it up. You're going to drag from the button to File's Owner like you normally would. The only difference is that this is happening in the cell class instead of the view controller class.

 


 

All that's left is to make the method do something. You can do a simple stub for now to make sure that this works:

- (IBAction)buttonPressed:(UIButton *)button
{
NSLog(@"It worked!");
}

 

Go ahead and run the app, and you should see this message in the console when you hit any button. And now we reach the fun part. As you can see, this method fires when you hit any button. So, how do you know which button?

 

There are a couple of ways to answer this question and, like so many programming challenges, which way to go will depend on your situation. You could use the tag property of the button. This is a common thing to do when you have multiple buttons and would like to distinguish between them. You would need to declare a property in the cell for the button, and then you could assign the tag value in cellForRow like this:

 

[[cell theButton] setTag:[indexPath row]];

 

Then you extract the tag value in the IBAction method:

 

NSLog(@"Pressed button at row: %d", [button tag]);

 

For many tables, this will be an adequate and simple approach. However, if you recall some behaviors from Part 3 regarding multi-section tables, then your spidey sense should be warning you that something may be wrong here. We are using the row parameter to identify the cell, but what happens if there is more than one section? You could have row 0 in the first section, and row 0 in the second section, and row 0 in the third section... Remember that row indexes reset in each section. This is why NSIndexPath provides TWO parameters - section and row - because only one parameter is insufficient to find/describe the location.

 

UIButton doesn't have an NSIndexPath property. If you really wanted to, you could subclass UIButton to add that property. Then the approach above would be the same, except you would provide/extract the indexPath instead of merely the row. I personally tend to view subclassing as a last resort, so I've never bothered doing this. But I suppose I could see some situations where BSTableButton could come in handy. But that isn't why I've brought you here today.

 

No matter what the approach, you have to do some work. Above we've done a little work in cellForRow, and a little work in the IBAction method. (And if you subclassed, you've done a lot of work in even more places) Also think in terms of performance. You are flicking your way through a table, and you want to get in and out of cellForRow as fast as possible. Assigning the tag (or indexPath) is just one more thing that has to be done that is preventing you from leaving the method ASAP. So instead of doing the work for each cell, just do the work when the user actually hits the button. The question is how.

 

In the IBAction, I receive the button itself as a parameter. From there, I would like to wind up with an indexPath. This will allow me to know that the 5th button was pressed, so I can go do something to the 5th item in the array. A quick glance at the UITableView documentation reveals a handful of methods that return an indexPath. For all visible rows. For the selected row (before you get excited, this is only for the blue highlighted row, not for tapping on items in the row). The only one that seems useful in this situation is indexPathForCell:. If I have a cell, the table can tell me where it is located.

 

Ok, so how do I get to the cell from the button. Ah ha! Remember that these are all views, and that they have been arranged into an hierarchy. The button would be a subview of the cell, so since I'm starting with the button, I would need to walk UP the hierarchy. You do that with the superview method.

 

This part can require some learning and trial-and-error, so let's do it the hard way before we skip to the answer. Add this to the IBAction:

 

NSLog(@"superview is: %@", [[button superview] description]);

 

Run it and you should see something like this:

 

superview is: <UITableViewCellContentView: 0x4d31390; frame = (0 0; 320 43); layer = <CALayer: 0x4d313f0>>

 

Hrm, UITableViewCellContentView. We're looking for CustomTableCell, so that's not a match. If you look at the top of the UITableViewCell documentation, you'll find the contentView property mentioned, and then you can go read its description. Basically, what we added the button to was the contentView, not the cell directly. So we need to go higher in the hierarchy:

 

NSLog(@"superview is: %@", [[button superview] description]);
NSLog(@"superview superview is: %@", [[[button superview] superview] description]);

 

Try again:

 

superview is: <UITableViewCellContentView: 0x4d31390; frame = (0 0; 320 43); layer = <CALayer: 0x4d313f0>>
superview superview is: <CustomTableCell: 0x4b3ad70; baseClass = UITableViewCell; frame = (0 264; 320 44); autoresize = W; layer = <CALayer: 0x4b39e20>>

 

Bingo. So, with this particular hierarchy, to go from the button to the cell, we need to go up 2 levels in the view hierarchy. This will depend on your actual view hierarchy, which is why I showed the long steps first. Just keep walking up the hierarchy until you find the cell. (I'm sure you could come up with an algorithm to keep walking, but it's never been worth the effort for me) Now that we know where we're going, our actual line of code is:

 

CustomTableCell *cell = (CustomTableCell *)[[button superview] superview];

 

We have the cell, and can now get the indexPath:

 

NSIndexPath *indexPath = [[self mainTableView] indexPathForCell:cell];
NSLog(@"indexPath is: %@", [indexPath description]);

 

Now you have the exact same location that you would have in cellForRow. So you can access your array the same way, and go manipulate your data or otherwise make something happen as you see fit.

 

Cell Identifiers Suck

 

I mentioned in Part 4 that I don't like the way Apple put the cell identifier into the cell's XIB. About a year ago, I discovered a blog post by Jeff LaMarche that provides a workaround. I won't bother repeating what he has said there, so let's just apply the code with a little twist.

 

Add this to CustomTableCell.h:

 

+ (NSString *)reuseIdentifier;

 

And then add these to CustomTableCell.h:

 

- (NSString *)reuseIdentifier
{
return [[self class] reuseIdentifier];
}

+ (NSString *)reuseIdentifier
{
return NSStringFromClass([self class]);
}

 

It's the same thing Jeff shows, I just took out the constant declaration as mentioned in the comments. This allows for easy copy-pasting into any cell class.

 

You can now blank out the identifier setting in IB, or forget to include it altogether.

 

And Now For Something New

 

There are a variety of debates regarding XIB-based cells. First there are the usual arguments with the "I only use code, IB is the debbil" people. Then there is another layer for use in cells, with people arguing that XIBs are slower than code because you have to go do disk to load the cell. There must have been some truth to this, because Apple added a new class for OS 4 to address it. It's too bad it took so long, because if anything we needed it more in slower-hardware days of OS 2 than we do today, but they felt it was important enough to create, so we should at least give it a look.

 

Now presenting: UINib.

 

Read the documentation and you'll find that it is basically a caching and instantiation class for loading XIBs. You can use it for any kind of XIB loading, but you can be pretty confident that it was developed for table cells. It doesn't wind up making a tremendous difference in terms of code - if anything it adds some - but I'll take Apple at their word that this is faster in terms of performance. I like using UINib as a property, so add this to BasicViewController.h:

 

@interface BasicViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>
{
}

@property (nonatomic, retain) IBOutlet UITableView *mainTableView;
@property (nonatomic, retain) IBOutlet CustomTableCell *customTableCell;
@property (nonatomic, retain) UINib *customTableCellNib;

 

And then I implement it with a custom getter in the .m file:

 

@@synthesize customTableCellNib = ivCustomTableCellNib;
...

// dealloc
[self setCustomTableCellNib:nil];
...

- (UINib *)customTableCellNib
{
if (ivCustomTableCellNib == nil)
{
ivCustomTableCellNib = [[UINib nibWithNibName:NSStringFromClass([CustomTableCell class]) bundle:nil] retain];
}
return ivCustomTableCellNib;
}

 

I customize the getter so that I don't have to care who creates it or when. The first time it is needed, it will get created.

 

So we've added a property, and we've added a method. What's the payoff? Well, not much really. Here is what we had before:

 

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

 

This now becomes:

 

if (cell == nil)
{
[[self customTableCellNib] instantiateWithOwner:self options:nil];
cell = [self customTableCell];
[self setCustomTableCell:nil];
}

 

Wow. We changed one line of code. Yay. Well, this is more about performance than it is about code reduction.

 

A Couple Of Tweaks

 

Speaking of performance, there is one thing you want to be careful of in cell design, whether it is in code or via XIB: avoid transparency. With our fancy iPhone 4's and iPad 2's, this is becoming less and less of an issue, but the basic premise remains the same: the device likes opacity a whole lot more than it does transparency.

 

What this specifically means as far as the view items go is to turn on the Opaque toggle, and supply a background color. The Opaque toggle is here:

 

 

When you first turn it on, the items will often default to black. Simply apply a different color:

 

 

Naturally this won't work if your background is not consistent, like a gradient or an image. In those cases, you're pretty much stuck with the transparency.

 

The next thing to be aware of isn't performance related, but it is very visual so you'll want to make sure to catch it. For labels (and some others like image views) you will want to specify a highlight color:

 

 

This is the color that the text will be when the user highlights a row. Generally speaking you will want it to be white, but that's a design decision for you. At some point in the last couple releases of Xcode, and I'm not sure when this started, the default automatically became white. So hopefully you shouldn't have to mess with this too much. But if you're on a slightly older version of Xcode, be aware that the default used to be black. Regardless, if you tap a cell, and don't see nice white text, this is where you need to go to fix it.

 

Holy Crap, This Is Long

 

Now you know why I split this into 2 parts. Head back to the cell's XIB, and we're going to make a slight change. Make the cell taller. You can add more stuff if you want, but for immediate purposes, all I want is something like this:

 

 

Depending on how you set up the label's masks, when you run the app now you should get something like this:

 

 

Well that's no good. What happened here? I made the cell taller, why isn't it displaying that way? Ah, because nobody told the table that the row height should change. So we need to do that.

 

The wrong answer is to use this delegate method:

 

tableView:heightForRowAtIndexPath:

 

This method should ONLY be used if your table will feature more than 1 row height. If you have tall rows and short rows and everything in between, then this method is pretty much your only option. But if your rows are all the same height, then you will slam against this method over and over and over again (seriously, add some logs and watch how much it gets called) just to return the same number each time.

 

Fortunately, there is a better answer: UITableView has a rowHeight property. In fact, we've already utilized it, we just didn't realize it. Open up the BasicViewController.xib, select the table view, then hit Cmd-3. Right there at the top of the inspector: row height of 44.0. So we could simply change this number to match whatever our cell height is, and things will be good.

 

Or will they? Hard-coding numbers is generally something to avoid. And really, we could change the cell design again, and change the height again, and then we just forget to come back and change this setting. Wouldn't it be nice if we could establish a link between our cell height and our row height?

 

We can't quite get there directly, but we sure can take a measurement and use that to define the row height.

 

So first we need a cell. viewDidLoad is a good place to do this, and we are literally going to do the same thing we do in cellForRow to get a cell:

 

[[self customTableCellNib] instantiateWithOwner:self options:nil];

 

From what we learned before, we know that the cell property is now occupied. So we can grab that cell, take a measurement, and drive the table:

 

[[self customTableCellNib] instantiateWithOwner:self options:nil];
[[self mainTableView] setRowHeight:[[self customTableCell] frame].size.height];
[self setCustomTableCell:nil];

 

We get the frame of the cell, then get the height, and give that value to the rowHeight property. Then we clear out the cell property since we're done with it.

 

Try again:

 

 

Muuuuch better. Now you can change the cell's height as much as you want, and the table will always display the correct row height.

 

Thus concludes a couple of lengthy posts on this topic. But don't let that volume scare you away from using XIB-based cells. After you've run through the process a few times, it gets less intimidating. And once you're there, you get to take full advantage of Interface Builder's promise to make view layout easier.

 

TableViewTutorial_Part5.zip

UITableView How-To: Part 4 - XIB-Based Cells

Part 1 | Part 2 | Part 3 | Part 5Part 6

 

Up to this point in the series, the focus has been on basic table and data structures. Now let's take a look at customizing the appearance of the table view.

 

I'm not intending to spend much time talking about standard cells, but there actually are a number of ways to customize cells without needing to subclass. UITableViewCell's initWithStyle: method accepts a parameter for which Apple has provided several standard options. You can show text in several places, and you can add images and accessories. Take a quick glance at the Settings app on your phone; those are standard cells. So before you dive into subclassing cells, make sure you are aware of the built-in options. They can save you a lot of time. This post at Cocoa With Love is definitely worth reading.

 

But, let's assume that those standard configurations are inadequate for your awesome table design. Or perhaps you've seen another app that displays lots of crazy things in a table, and you wonder how it was done. Chances are that the answer will be the same either way: custom table cells. Like most visual things in Cocoa, there is a code-based approach and an Interface Builder-based approach. I'm going to focus on using IB, as the layout is significantly easier, although the setup has some nuances. This approach is based on the Apple sample project called TaggedLocations.

 

Overview

Let's take a quick look at the players involved, because the process to set this up is a tad convoluted:

ViewController.h
ViewController.m
ViewController.xib

TableCell.h
TableCell.m
TableCell.xib

 

In the same way that you would reference a button in your view by creating an IBOutlet, the same thing will be done here. So the view controller will have an IBOutlet for the cell:

@property (nonatomic, retain) IBOutlet TableCell *tableCell;

 

In order to do this without causing any build errors, the cell class needs to exist. But this outlet needs to exist in order to to complete the TableCell.xib, so there is something of a chicken-and-egg situation. The basic steps are:

 

1. Create the view controller class
2. Create the cell class
3. Create the IBOutlet in the view controller
4. Design the cell
5. Rewire the table delegate methods to use the cell

 

So there is a fair amount of back-and-forth between the classes, but after you get used to it, it's not so bad. Let's begin…

 

1. Create a view controller. I'm calling this one BasicViewController, but you can use whatever you want. Don't bother getting too hung up with delegate methods for now.

 

2. Add a new file. This will be a UITableViewCell subclass, and it's not immediately clear how to do this. Just choose Objective-C class, and then select UITableViewCell from the pull-down menu:

 

 

Call it CustomTableCell. This will create the .h and .m file, but we want a XIB file, too. So add a new file for that as well. Select User Interface at the side, and then choose a View XIB. iPhone-vs-iPad doesn't really matter, but I tend to go with iPhone.

 

 

Give it the same CustomTableCell name. You should now have .h, .m, and .xib files for CustomTableCell.

 

3. Now create an IBOutlet for the cell in the view controller. You will need @class in the .h file and #import in the .m class. The highlights of what you should wind up with are:

 

// BasicViewController.h
#import <UIKit/UIKit.h>
@class CustomTableCell;

@interface BasicViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>
{
}

@property (nonatomic, retain) IBOutlet UITableView *mainTableView;
@property (nonatomic, retain) IBOutlet CustomTableCell *customTableCell;

 

// BasicViewController.m
#import "CustomTableCell.h"

@implementation BasicViewController

@synthesize mainTableView = ivMainTableView;
@synthesize customTableCell = ivCustomTableCell;

- (void)dealloc
{
[self setMainTableView:nil];
[self setCustomTableCell:nil];
[super dealloc];
}

 

4. Configure the cell. Open up CustomTableCell.xib, as we have some preliminary things to take care of. You should see:

File's Owner
First Responder
View

Select View and delete it. Now go to your Library palette, find the Table View Cell, and drag it to the spot where View was.

 

 

Now we need to change classes. Select File's Owner, and go to the inspector panel for Identity (Cmd-4). Change the class to BasicViewController. Now select the table view cell, and in the inspector change the class to CustomTableCell.

 

 

Now we need to connect the IBOutlet we made earlier. Ctrl-click on File's Owner. Select the customTableCell outlet, and drag that to the CustomTableCell item.

 

 

Hey, wait a second... this isn't my view controller's XIB! You are correct. But the view controller will create this class - it will be the file's owner - so it does make some sense to do this. But don't get carried away. For example, you do see the view outlet, but don't mess with that here as that is being populated in your view controller's XIB. We are doing this for one reason and at this point one reason only: to get access to that cell IBOutlet.

 

There is a lot of customization we could potentially do here, but in the interest of quickly moving along to see this in action, simply drop a couple of a labels onto the cell. We'll come back later to wire everything up.

 

 

5. Configure the table delegate methods. Return to BasicViewController, and for now tell it there are 10 rows. The important change happens in cellForRow:

 

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

CustomTableCell *cell = (CustomTableCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
[[NSBundle mainBundle] loadNibNamed:@"CustomTableCell" owner:self options:nil];
cell = [self customTableCell];
[self setCustomTableCell:nil];
}
return cell;
}

 

For the most part, this is pretty similar to what we've done before. Instead of a UITableViewCell, we're using a CustomTableCell, so we change the class types to reflect that. We're still asking for an available cell, and if one isn't available then we create one. The creation part is different than we've done before, naturally since we aren't just using code this time.

 

The first thing we do is load the XIB file. We indicate which class, and who should own it. The next thing we do is wave our hands and say there is some black magic happening here. Then we assign the cell property to our cell variable. Hrm, what? How did that get there? Well, let's back up and address that black magic.

 

The documentation has this to say about loadNibNamed:owner:options:

During the loading process, this method unarchives each object, initializes it, sets its properties to their configured values, and reestablishes any connections to other objects.

 

For our purposes, the important part is the last bit.

 

Think about what you do with a normal view controller. You place, say, an image view in IB, you create an IBOutlet for it, and then make the connection in IB. So after you instantiate your view controller, what happens? The XIB is loaded, which means an image view is also instantiated, and this image view is then assigned to your IBOutlet/property. When you go to talk to your image view using the property - [[self imageView] setImage:...]; - the image view is there already.

 

What we're doing here is exactly the same, only splitting things up into separate files. Instead of an image view, it's a table cell, and instead of being in the view controller's XIB, it is in a separate XIB. But the act of loading the XIB causes the outlets to be populated, so we end up with the same result. Black magic indeed.

 

So:
1. We load the XIB file
2. The IBOutlet gets populated with the cell
3. We assign that cell to our local cell variable
4. We clear out the property since we don't need to keep it around

 

Go ahead and run the app at this point, and if you've wired everything correctly, you should see:

 

Configure the cell

 

Let's circle back around and finish up the cell. First of all, we skipped a really important step. Recall what the initializer for a standard cell looks like:

 

cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];

 

We pass two parameters: a style, AND a reuse identifier. Now compare to how we just created the cell:

 

[[NSBundle mainBundle] loadNibNamed:@"CustomTableCell" owner:self options:nil];

 

Uh oh, no reuse identifier parameter. That's a problem if we want good table scrolling performance (and we do). I wish Apple would have handled this differently, but what they did is imbed the reuse identifier into the XIB file. So open it back up, and select the cell. Then go to the inspector panel again, Cmd-1.

 

 

It will be blank when you first look at it, so you will need to type it in. The important part here, and one unfortunate aspect of Apple's decision to do this, is that whatever you type in here, needs to match what you type in here:

 

static NSString *CellIdentifier = @"CustomCellIdentifier";

 

So you have to make sure the same thing is typed in 2 places, and if you screw up either one then you won't recycle cells. This will hurt scrolling performance. I really wish Apple had gone with more of an initWithNibName:bundle:reuseIdentifier: approach for these cells. Oh well.

 

Let's add some properties to the cell so that we can talk to the labels.

// CustomTableCell.h
@interface CustomTableCell : UITableViewCell
{
}

@property (nonatomic, retain) IBOutlet UILabel *redLabel;
@property (nonatomic, retain) IBOutlet UILabel *greenLabel;

 

// CustomTableCell.m

@implementation CustomTableCell

@synthesize redLabel = ivRedLabel;
@synthesize greenLabel = ivGreenLabel;

....

- (void)dealloc
{
[self setRedLabel:nil];
[self setGreenLabel:nil];
[super dealloc];
}

 

Standard stuff here. Where it gets tricky is actually making the connections in IB. You are probably accustomed to dragging from File's Owner to establish IBOutlet connections. Ah, but remember which class we're dealing with here. File's Owner is the view controller, but we are adding these IBOutlets to the cell. So you can drag from File's Owner all that you want, but you won't be able to create the links. You have to drag from the cell class to the labels:

 

 

Now let's head back to the view controller and put some data in the labels. I'm not going to bother setting up any data; refer to Part 3 for some thoughts on how to arrange your data for this purpose. For now, just drop in something so that you can see different text in each field:

 

   ...

NSUInteger row = [indexPath row];
[[cell redLabel] setText:[NSString stringWithFormat:@"Red %d", row]];
[[cell greenLabel] setText:[NSString stringWithFormat:@"Green %d", row]];

return cell;
}

 

With standard cells, you are talking to [cell textLabel] or [cell detailTextLabel]. Same idea, just using the properties that you've created.

 

Your cell is a blank canvas ready to be customized to your heart's delight. Want 5 labels? Good. Want 10 images? Great. Go nuts.

 

This is turning into a longer post than I thought, and I still have a lot to talk about. So I'm going to split this up into 2 posts. Tune in later for the sequel.

 

TableViewTutorial_Part4.zip