Archive

Archive for January, 2010

Tap-enabled UITableView section headers (and footers)

January 31st, 2010 Comments off

Displaying ads

For a project I’m working on, I had the need to display inline ads within a UITableView of news.

My initial thought was simply to create a different kind of cell every 10 cells, and that cell knows how to display itself:

- (UITableViewCell*) tableView: (UITableView*) atableView 
  cellForRowAtIndexPath: (NSIndexPath*) indexPath 
{
  static NSString *NewsCellIdentifier = @"NewsCell";
  static NSString *AdCellIdentifier = @"AdCell";
  
  UITableViewCell *cell = nil;
    
  // Set up the cell...
  if (0 == indexPath.row % 10)
  {
    CMAdImageTableViewCell *adCell = 
      (CMAdImageTableViewCell*)[tableView dequeueReusableCellWithIdentifier: AdCellIdentifier];
    if (adCell == nil) 
    {
      adCell = [[[CMAdImageTableViewCell alloc] 
        initWithStyle: UITableViewCellStyleSubtitle 
        reuseIdentifier:AdCellIdentifier] autorelease];
    }
    /* more cell customization... */
    cell = adCell;
  }
  else
  {
    CMNewsTableViewCell *newsCell = 
      (CMNewsTableViewCell*)[tableView dequeueReusableCellWithIdentifier: NewsCellIdentifier];
    if (newsCell == nil) 
    {
      newsCell = [[[CMNewsTableViewCell alloc] 
        initWithStyle: UITableViewCellStyleSubtitle 
        reuseIdentifier: NewsCellIdentifier] autorelease];
    }
    /* more cell customization... */
    cell = newsCell;
  }
  
  [cell setNeedsDisplay];
  
  return cell;
}

This works. There is a CMAdImageTableViewCell created every 10 cells. And since the UITableView delegate can get the -(void) tableView: didDeselectRowAtIndexPath: message, you have a chance to intercept a tap on the ad cell and send the user to the ad’s website.

However, you have to do some bookkeeping. For instance, the total number of cells will be the sum of all the news cells and your ad cells, or [news count] + [news count] % 10. You also have to find the proper offset in your model when you get an indexPath, etc. Not difficult, but annoying.

Another issue is that TableView cells all look and behave the same. An Ad cell doesn’t say “I’m an ad”, it just says “I’m another cell”, by the way it scrolls. Your ad design will probably be different than your news design to tell them apart, but it would be nice to have another, subtle way to differentiate the ads.

Section Headers

UITableViews can be separated in sections, with an optional header and footer. These sections stay on screen, at the top and/or bottom, and are overlaid on top of the UITableView’s actual data. They can be semi-transparent, allowing to to see the table’s data underneath. You can see sections in action in many apps, including the Contacts application on your iPhone: notice the section headers “A”, “B”, “C”.. as you scroll down?

To create sections, just return the number of sections in your delegate’s -(NSInteger) tableView: numberOfRowsInSection:. Once you specify more than one section, you can set a section header’s appearance by responding with a UIView to -(UIView*) tableView: viewForHeaderInSection: in your UITableView’s delegate.

This is a full-fledged UIView, so you can do things like drawing with Core Graphics or adding a complete view hierarchy. For the simple case of an ad, if you already have a PNG with the ad to display, you can simply do:

- (UIView*) tableView: (UITableView*) tableView 
  viewForHeaderInSection: (NSInteger) section 
{
  /* assumes your tableview is 320 wide, makes a section header 80 pixels high */
  customView = [[[UIView alloc] initWithFrame: CGRectMake(0.0, 0.0, 320.0, 81.0)] autorelease];
 
  UIImageView *imgView = [[[UIImageView alloc] initWithImage: myPNGImage] autorelease];
  /* makes the views slightly transparent so you can see the cells behind them as you scroll */
  imgView.alpha = 0.7;
  customView.backgroundColor = [UIColor colorWithRed: 1.0 green: 1.0 blue: 1.0 alpha: 0.7];
  
  [customView addSubview: imgView];
  
  return customView;
}

I made the header with a semi-transparent white background so you can see a little bit of the underlying news as you scroll by, giving the overlay a nicer appearance. You can adjust it to suit your needs.

This is great. The ads stay on-screen, on top of the content, and they don’t interfere with it.

There’s only one problem…
You can’t tap on section headers. There is no delegate method to tell you what section header was tapped.

UIButton to the rescue

What if, instead of embedding a UIImageView, you embedded a UIButton in your customView? UIButton is a subclass of UIView, but it handles events and can dispatch on target-action. Plus, you can add an image to a button! Let’s try this:

- (void) headerTapped: (UIButton*) sender
{
  /* do what you want in response to section header tap */
}
  
- (UIView*) tableView: (UITableView*) tableView 
  viewForHeaderInSection: (NSInteger) section 
{
  customView = [[[UIView alloc] initWithFrame: CGRectMake(0.0, 0.0, 320.0, 81.0)] autorelease];
  customView.backgroundColor = [UIColor colorWithRed: 1.0 green: 1.0 blue: 1.0 alpha: 0.7];
 
  /* make button one pixel less high than customView above, to account for separator line */
  UIButton *button = [[[UIButton alloc] initWithFrame: CGRectMake(0.0, 0.0, 320.0, 80.0)] autorelease];
  button.alpha = 0.7;
  [button setImage: myPNGImage forState: UIControlStateNormal];
  
  /* Prepare target-action */
  [button addTarget: self action: @selector(headerTapped:) 
    forControlEvents: UIControlEventTouchUpInside];
  
  [customView addSubview: button];
  
  return customView;
}

The difference with UIImageView is the target-action method. We added a new target (self), with the method headerTapped: (defined above) for the event UIControlEventTouchUpInside. You can view all events handled by UIButton, but I recommend TouchUpInside because it is sent when the user lifts her finger while inside your button. If she slides out of the button the action is not sent and she can effectively “cancel” the button’s activation.

Voi|à! You now have a section header that responds to taps.

Notes

This code uses hard-coded numbers for example purposes. Use macros or consts in your own code, not magic numbers!

Also, creating custom UIViews is expensive. You don’t want to do it every time viewForHeaderInSection: is called as this will impact performance. Once you create a UIView, save it in a mutable array or dictionary and cache it for the next time you are called.

Categories: Development, MacOSX Tags: