Cell-Based Buttons
Simulating a button in a cell has one big advantage: it's super easy to do. If you don't care about it looking fancy, or want to bunch up several "buttons" in one section, as the one above for editing a contact, then a cell-based button is a good option. For simple black on white text, all you have to do is set the cell.text property to your button text. Since the visual aspects of a table cell can be customized quite extensively, you really have lots of flexibility here. However, once you go beyond the standard UITableCell object, the complexity of your code increases. I believe that once you have gone beyond the basic black on white text, you might as well go with a real button.
You can't have two table cells side by side like that, so these have to be buttons. You'll note that these rounded corners match the rounded corners of the table cells. I think these buttons use the normal RoundedRect button, as opposed to custom buttons. Custom buttons are mostly needed when you want a background color other than white. But if all you need are plain white backgrounds, you can use the RoundedRect buttons. Which leads to the other advantage of using real buttons: better control over background colors. Setting a background color in a grouped table cell is difficult. Just simply setting the background property to a color is not enough, as the rounded corners do not get the color. The only solution I've seen to this problem is a complicated bit of code to color in the curved corners (as demonstrated here). You also don't get the cool gradients as you see in the two red buttons on the top two examples.
There are probably other advantages to using real buttons, but to me these are enough to force me to use buttons sometimes. Implementing
How a cell-based button works is that your code simply traps the tableView:didSelectRowAtIndexPath: message. You associate the section and row with which "button" was hit and perform whatever action that button is supposed to do. For example, from the image above, you can associate that block of buttons with section #1, and row #0 is the "Clear History" button, row #1 is "Clear Cookies", etc. One thing to remember is that the action of touching a cell automatically highlights the cell. Before you do anything else, you should turn off that highlight. Here is some example code of how we might implement the above section of three buttons. There are two aspects here: setting up the buttons for display, and handling the actions when the buttons are selected. Setting up for display. All of this is fairly boilerplate code for the tableView:cellForRow: message. The big difference here is that we are not fetching the row contents from a database, but hard-coding the button title. This can easily be internationalized as well.- (UITableViewCell *)
tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
}
// are we setting up our button section?
if(indexPath.section == 1) {
switch(indexPath.row) {
case 0: cell.text = @"Clear History"; break;
case 1: cell.text =@"Clear Cookies"; break;
case 2: cell.text = @"Clear Cache"; break;
}
}
else {
// do whatever for the other sections
}
return cell;
}
For cell-based buttons, you want to avoid using the accessory view, like putting in a detailed disclosure button or the checkmark. Having these items on a "button" will be confusing. Handling actions. All you do here is figure out which "button" was hit and do whatever it is you need to do. Also notice the code to turn the highlight off. - (void)
tableView:(UITableView *) aTableView didSelectRowAtIndexPath:(NSIndexPath *) indexPath
{
// remove the row highlight
[aTableView deselectRowAtIndexPath:indexPath animated:YES];
// Is this section our "button" section?
if(indexPath.section == 1) {
switch(indexPath.row) {
case 0: [self doClearHistory]; break;
case 1: [self doClearCookies]; break;
case 2: [self doClearCache]; break;
}
return;
}
else {
// do other stuff...
}
}
Real Buttons
Real buttons offer all the advantages you get with a real button. For one thing, the size of the button can be fully customizable. This allows you to have two buttons side by side, as in this example from showing detailed info of a contact:Implementing
So how are UIButtons integrated into a UITableView? Again, there may be several ways, but I find the easiest is to sneak the buttons into a section header. Each section can have a header view. In this view is where you put your UIButton objects. The section itself has no data rows, only a header. So what I do is accommodate for one extra section in my table, but return 0 for the number of rows.- (NSInteger)
numberOfSectionsInTableView:(UITableView *)aTableView
{
// return the number of sections the data is organized in, plus 1 for the button
return 2 + 1;
}
Assuming you want your button at the end of the table, you would use code like this to return the number of rows: - (NSInteger)
tableView:(UITableView*) aTableView numberOfRowsInSection:(NSInteger) sectionNum
{
if(sectionNum == 3) {
// this is our button section
return 0;
}
else {
// do whatever, according to the data
}
}
Now we have to create our buttons. This is done in the tableView:viewForHeaderInSection: - (UIView *)
tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)sectionNum
{
if(sectionNum == 2) {
// create the parent view that will hold 1 or more buttons
UIView* v = [[UIView alloc] initWithFrame:CGRectMake(10.0, 0.0, 300.0, 44.0)];
// create the button object
UIButton* b = [UIButton buttonWithType:UIButtonTypeCustom];
[b setBackgroundImage:[[UIImage imageNamed:@"redbutton.png"] stretchableImageWithLeftCapWidth:12.0 topCapHeight:0.0] forState:UIControlStateNormal];
[b setBackgroundImage:[[UIImage imageNamed:@"bluebutton.png"] stretchableImageWithLeftCapWidth:12.0 topCapHeight:0.0] forState:UIControlStateHighlighted];
b.frame = CGRectMake(10.0, 0.0, 300.0, 44.0);
[b setTitle:@"Button Title" forState:UIControlStateNormal];
// give it a tag in case you need it later
b.tag = 1;
// this sets up the callback for when the user hits the button
[b addTarget:self action:@selector(action:) forControlEvents:UIControlEventTouchUpInside];
// add the button to the parent view
[v addSubview:b];
return [v autorelease];
}
else {
// stuff for other sections
}
}
The values for the frames are specific to portrait mode; landscape mode will need some modifications. To add more buttons in this view, just adjust the y parameter in the frame for the button. I typically add 46.0 from the last one. This code makes use of two button images: b = [UIButton buttonWithType:UIButtonTypeRoundedRect];
You don't need to set the background images. If you want to create two side by side buttons, all you have to do is monkey around with the frames of the buttons so that they are side by side instead of on top of each other. There is one gotcha I have discovered using this mechanism, and I think it is an SDK bug. If your table takes up more than one screen and you have to scroll to see your button, some of the button may remain hidden under the tab bar or tool bar. You won't be able to scroll the entire header view into view. This is especially noticeable if you have 2 or more buttons stacked on top of each other. I think Cocoa Touch is confused by there being no data rows, or maybe it can't quite figure out how high the header view is. So the way around it is to tell how high your view is. Once you do this, the view is no longer partially hidden: - (CGFloat)
tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)sectionNum
{
return 46.0;
}
Of course, if you have more buttons in this section, you would return the appropriate height. There you have a quick tutorial on putting buttons in a table. How you choose which mechanism to use is up to you. My own guidelines are as follows: - If I want a simple black on white button: use a cell-based button
- If I need a colored button: use a real button
- If I want multiple buttons per row: use real buttons
Hi JES
ReplyDeleteI am trying to implement the code in viewForHeaderInSection....
The button goes in ok but the titles for the other sections are missing. You have added a comment '// stuff for other sections'
Can you expand on what should go there. I have tried to return a view with a label in it but things dont look so good. The label overlaps and the cells dont space out anymore.
Thanks
Ah, good question! I also ran into this problem, but forgot to mention it. For other sections where I didn't want to return a specific view for, I returned nil. So for the "stuff for other sections" should simply be:
ReplyDeletereturn nil;
Please give this a try and let me know if it works for you. If it does, I will update the article.
Thanks!
works for me
ReplyDeleteAwesome post dude - really really helpful. This seems to be the cleanest way to pull this off in a UITableView.
ReplyDeleteI was banging my head against the wall on the opacity of the edges of the buttons UNTIL I read closely when you said to EXPORT the button bitmap image from Inkscape. I was 'saving as' a PNG and it was looking like complete crap.
Thanks again! -jeff
Nice work. Thanks.
ReplyDeleteI am having a problem where the button is not firing every time and the blue background is not shown on highlight.
Any thoughs?
I had this problem it was because i didnt have the code below set. make sure you add your
Deleteif (sectionNum == x) // x should be the section header you have the button on..
- (CGFloat)
tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)sectionNum
{
return 46.0;
}