iPhone: how to create a transparent table header

March 1, 2009 · Posted in Tech 

A couple of weeks ago I was wondering how you created a transparent header for a table view on the iPhone like the one that’s in the built in contacts app detail view. Here’s a video of the contacts app so that you can see what I’m trying to achieve:

Initially, I thought that the way to do this was to add an additional section to the table and add a custom cell to it. That kind of works, but is quite hard work. I think I’ve found a better and simpler way – by using the tableView:viewForHeaderInSection method on UITableViewDelegate.

To start with, we need to create a basic view-based Cocoa Touch app in XCode. I’ve called mine ContactStylee. I’ve zipped up the completed XCode project for you so that you can follow along.

Once we’ve got the basic project template, we need to implement the UITableViewDataSource in our ContactStyleeViewController. As usual, this consists of implementing tableView:numberOfRowsInSection and tableView:cellForRowAtIndexPath. I also create a simple NSArray to hold some sample data in the viewDidLoad method. Here’s the interface file:

@interface ContactStyleeViewController : UIViewController
	<UITableViewDataSource, UITableViewDelegate> {

	NSArray *data;
	ContactStyleeAppDelegate *appDelegate;
}

@property (nonatomic, retain) NSArray *data;

@end

And the implementation:

@implementation ContactStyleeViewController

@synthesize data;

- (void)viewDidLoad {
   	[super viewDidLoad];
	self.data = [NSArray arrayWithObjects:
		@"First", @"Second", @"Third", @"Fourth", nil];
}

- (NSInteger)tableView:(UITableView *)tableView
	numberOfRowsInSection:(NSInteger)section {

	return [self.data count];
}

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

	UITableViewCell *cell = [tableView
		dequeueReusableCellWithIdentifier:@"normalcell"];
	if(nil == cell) {
		cell = [[[UITableViewCell alloc]
			initWithFrame:CGRectZero
			reuseIdentifier:@"normalcell"] autorelease];
	}
	cell.text = [self.data objectAtIndex:indexPath.row];
	return cell;
}

- (void)dealloc {
	[data release];
	[super dealloc];
}
@end

All pretty standard stuff. In the NIB, I’ve set the table to have the “Grouped” style. Here’s what that looks like when we run it in the simulator:

Conventional Grouped Table

So now we need to create the header view. I did this by adding a new View NIB to the project, calling it TableHeader. I then created a new view controller class, called TableHeaderViewController, and then set the class for the File’s Owner in the TableHeader.xib file to the new controller class. You need to remember to tell the File’s Owner about the view. I added an image and a couple of labels. For the purposes of this demo project, that’s all that’s needed in the header view: we won’t bother hooking it up to the data source for now.

To be able to reference the view from the ContactStyleeViewController, I added a new view controller to MainWindow.xib and made its class TableHeaderViewController, and we tell it to load the view from TableHeader.xib. I then added an IBOutlet of type TableHeaderViewController to ContactStyleeAppDelegate. This means that I can load that controller’s view from the app delegate.

Now that we’ve got the controller outlet and property in our app delegate, we need to hook that up in IB. So we make the new view controller that we just added to MainWindow.xib point to the new IBOutlet that we just added in the ContactStyleeAppDelegate. Here's how the app delegate interface file looks now:

#import <UIKit/UIKit.h>

@class ContactStyleeViewController;
@class TableHeaderViewController;

@interface ContactStyleeAppDelegate : NSObject <UIApplicationDelegate> {
	UIWindow *window;
	ContactStyleeViewController *viewController;
	TableHeaderViewController *headerViewController;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet ContactStyleeViewController *viewController;
@property (nonatomic, retain) IBOutlet TableHeaderViewController *headerViewController;

@end

Now we need a way for the ContactStyleeViewController to access our TableHeaderViewController. To do this, we add an IBOutlet of type ContactSyleeAppDelegate to our ContactStyleeViewController and then connect that up to our app delegate in IB. Here's the revised ContactStyleeViewController interface file:

#import <UIKit/UIKit.h>
@class ContactStyleeAppDelegate;

@interface ContactStyleeViewController : UIViewController
	<UITableViewDataSource, UITableViewDelegate> {

	NSArray *data;
	ContactStyleeAppDelegate *appDelegate;
}

@property (nonatomic, retain) NSArray *data;
@property (nonatomic, retain) IBOutlet ContactStyleeAppDelegate *appDelegate;

@end

Note that we need to forward declare the ContactStyleeAppDelegate class rather than importing the header file in order to avoid a circular include. Once we've hooked up this outlet to the app delegate in IB, we're almost ready to go. We just need to tell the table view where to get the header view from.

To do this, we need to implement two new methods in ContactStyleeViewController that are optional methods on the UITableViewDelegate protocol, tableView:viewForHeaderInSection and tableView:heightForHeaderInSection. These are pretty simple methods now that we've hooked up the app controller outlet:

- (UIView *)tableView:(UITableView *)tableView
	viewForHeaderInSection:(NSInteger)section {

	return [[appDelegate headerViewController] view];
}

- (CGFloat)tableView:(UITableView *)tableView
	heightForHeaderInSection:(NSInteger)section {

	return 85;
}

At this point, I reckoned I was done, so I built and ran the app in the simulator. What I saw wasn't quite what I had expected:

not_transparent.png

The header cell has an opaque white background, which is not what we wanted at all. We need to make a few simple adjustments in IB. Here's what the inspector looks like when the changes have been made (and you need to be careful that its the view that's selected in IB, not one of the controls):

ib_settings.png

You need to make sure that the "Opaque" checkbox is cleared, and that you set the background colour to have a 0% opacity. And that's it. Here's a screenshot:

finished_app.png

And here's a video showing that it scrolls just like the built-in contacts app:

It took me a while to figure out that this was the best way. Hopefully this post will help someone else to get there quicker. But, since I'm strictly an iPhone development beginner, I'd be really interested to hear from people if they think there's a better way to do it.

Comments

6 Responses to “iPhone: how to create a transparent table header”

  1. Nir on March 2nd, 2009 16:43

    Amazing – solved all my problems !!

  2. Chuck on April 3rd, 2009 00:40

    How would I go about pushing another view after clicking on a table row, when setup like this?

    I’ve tried: “[self.navigationController pushViewController:newViewController ];”

    But it doesn’t work.

  3. higgis on April 3rd, 2009 09:32

    Hi Chuck,

    You need to be doing this from a higher level in the view hierarchy, I think. Here’s what I do. On my application delegate class, I declare a method that gets called from the table delegate’s didSelectRowAtIndexPath: method. Here’s the app delegate method:

    - (void)thingWasSelected:(NSString *)thingName {
       [navController pushViewController:self.newViewController animated:YES];
    }

    And here’s the UITableViewDelegate method (note the cast to our concrete app delegate class):

    - (void)tableView:(UITableView *)tableView
           didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    
       [((MyAppDelegate *)[[UIApplication sharedApplication] delegate])
         thingWasSelected:[self.dataSource objectAtIndex:indexPath.row]];
    }

    We could add a category to UIApplication to return our concrete with a header file like this:

    // UIApplication+MyAppDelegate.h
    // forward declaration
    @class MyAppDelegate;
    
    @interface UIApplication (MyAppDelegate)
    - (MyAppDelegate *)concreteDelegate;
    @end

    And here’s the implementation file:

    // UIApplication+MyAppDelegate.m
    #import "UIApplication+MyAppDelegate.h"
    #import "MyAppDelegate.h"
    
    @implementation UIApplication (UIAppDelegate)
    
    - (MyAppDelegate *)concreteDelegate {
       return (MyAppDelegate *)self;
    }
    
    @end

    Then you just need to #import UIApplication+MyAppDelegate.h into the appropriate file and instead of the cast, do this:

    [[UIApplication sharedApplication] concreteDelegate];

    (I’ve written most of this code away from XCode, so please be on the lookout for typos!)

    Hope that helps.

    Cheers,
    James

  4. Win on July 14th, 2009 23:23

    Thanks for the great post! Though, wouldn’t it be easier just to create the view in a NIB and connect it to ContactStyleeViewController and return that view for the viewHeaderForSection method? Maybe something like:

    @interface ContactStyleeViewController : UITableViewController {
    IBOutlet UIView *headerView;
    }

    That way you wouldn’t have to use the delegate method and a separate controller just to get access to the header view?

    Just a thought. I may be way off-base.

  5. Jason McCreary on October 28th, 2009 01:09

    Nice article. I was looking to create something similar. Although I agree with what you have and the implementation, I feel like the contact app may indeed use a custom cell for this top section. The main reason for this belief is when you tap the edit button.

    I would appreciate any comments you could make on updating the UI to support this edit behavior.

  6. Billy on December 4th, 2009 11:09

    I agree with win it would be easier to create the view in a NIB and connect it to ContactStyleeViewController.

    Also, just to point out that your method (as is) wouldn’t work out with a table with multiple sections. You’d have to change viewForHeaderInSection to this

    if (section == 0)
    return [[appDelegate headerViewController] view];
    else
    return nil;

    The better way to do it would be to just do the following on viewDidLoad
    self.tableView.tableHeaderView = headerViewfromNIB;

    My 2cents

Leave a Reply