BuySellAds.com

Read the chapters in my new book Barcodes with iOS 7 as I hand them in. Great new app opportunities await!
Our DNA is written in Objective-C
Jump

How to Shrink Cells

When you look at the Contacts.app you see that Apple somehow manages to change the size of cells in UITableViews of grouped styling.

Original:

Copy:

If you search around on the internet most of the solutions getting such a look revolve around making your own UIView and adding this as headerView to a section. Now if you want the cell on the right side to still behave like a cell is expected to behave then you have lots of work ahead of you. So I consulted the twitterverse and here props have to go to Jason Terhorst who hit the bull’s eye:

If I recall correctly from Apple’s example code, they just clear the background, and redraw that rounded rect at new size. It’s been a while since I did this, but I think I swapped in my own custom backgroundView.

I researched and experimented for a day to get it perfect. What follows is a description of how to pull off this magic trick, so you can do so too. The end result will look like this:

(Heart Icon by DryIcons)

To make individual rows of a table view larger vertically you have to implement a delegate method that overrides the standard value. My photo cell is 63.0 pixels. We’re checking the indexPath so that we return this height only for our photo cell, all others get the standard tableView.rowHeight.

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
	if (indexPath.section == PARTNER_SECTION && indexPath.row == 0)
	{
		return 63.0;
	}
 
	// return standard height
	return tableView.rowHeight;
}

Since we need the image view for the photo to be persitent and independent from individual cell lifetimes we are instantiating it in the viewDidLoad for keeping as a IVAR reference. I played around with all these parameters until I had a pixel-perfect replica of the original in Contacts.app. I’m using the newly discovered method of adding border and round corners to the image view, something that was added in SDK 3.0, and VERY convenient. Note how it is possible to also have a semi-transparent black border which lets the photo shine through a little. Nice touch!

Apple actually has their own private class for it, but this is sufficient for our purposes. I omitted the definition of the IVAR and import of the QuartzCore header.

photoImageView = [[UIImageView alloc] initWithFrame: CGRectMake(9.0, 0, 64.0, 64.0)];
[photoImageView.layer setBorderColor:[UIColor colorWithWhite:0 alpha:0.5].CGColor];
[photoImageView.layer setBorderWidth:2.0];
[photoImageView.layer setCornerRadius:5.0];
[photoImageView setClipsToBounds:YES];
 
if (!partnerPhoto)
{
	photoImageView.image = [UIImage imageNamed:@"GenericContact.png"];
}
else
{
	photoImageView.image = partnerPhoto;
}

As promised, I’m not using heightForHeaderInSection or viewForHeaderInSection, we’re going for a REAL cell. There are two places where you can create and modify a cell: when the table view requests it from the delegate and just before it is shown on screen. The first is is the well known cellForRowAtIndexPath.

#define PARTNER_SECTION 1
#define PHOTO_INDENT 74.0
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	UITableViewCell *cell;
 
	switch (indexPath.section)
	{
		case PARTNER_SECTION:
		{
			NSString *CellIdentifier = @"PartnerCell";
 
			cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
			if (cell == nil)
			{
				cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
 
				// indent the contentView sufficiently
				cell.indentationLevel = 1;
				cell.indentationWidth = PHOTO_INDENT;
				cell.textLabel.font = [UIFont boldSystemFontOfSize:18.0];
 
				// add the photo to the tableView's content
				CGRect frame = cell.frame;
				photoImageView.frame = CGRectMake(frame.origin.x-1.0, frame.origin.y-1.0, 64.0, 64.0);
				[cell.contentView addSubview:photoImageView];
 
				// head sticker
				UIImageView *heartView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"heart.png"]];
				heartView.frame = CGRectMake(photoImageView.frame.origin.x + 40.0, photoImageView.frame.origin.y + 45.0, heartView.frame.size.width, heartView.frame.size.height);
				[cell.contentView addSubview:heartView];
				[heartView release];
			}
 
			if ([self.partnerName length])
			{
				cell.textLabel.text = self.partnerName;
			}
			else
			{
				cell.textLabel.text = @"Choose ...";
			}
 
			cell.detailTextLabel.text = @"Partner";
 
			// if we didn't add the photo in willDisplayCell then we would have to do it here
			// reloading for same strange reason makes the imageView disappear every other time
 
			[cell.contentView insertSubview:photoImageView atIndex:0 ]; // below heart
 
			cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
 
			break;
		}
 
		default:
		{
			NSString *CellIdentifier = @"NormalCell";
 
			cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
			if (cell == nil) {
				cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
			}
 
			cell.textLabel.text = @"Other Cell";
			break;
		}
	}
 
    return cell;
}

To shift the cell contents to the right we are using indentation. You specify an indentation level and the amount of pixels per level. This moves the textLabel and all other stuff out of the way. I learned that it was not sufficient to simply add the photo image view once and be done with it, for some strange reason this causes it’s contents to become invisible on every other return from the people picker. So I’m also re-inserting it every time the cell is requested. Does no harm, but fixes this problem.

An alternative to adding the photo to the cell is to add it as subview of the table view itself. This causes it to still be moved together with the table, but does not move when going into edit mode. But having it in the view hierarchy of the cell is more logical. Note that I am setting the photo origin to -1, -1 to line up with the edges of the contentView. 0,0 would actually be inside the cell borders and then the upper edge would no longer line up.

You better get a cup of coffee now, because that was the easy stuff. What’s still left is dealing with the border.

Apple has given UITableViewCell two properties that are of use to us. They have their own private class manning those for regular grouped table view cells. cell.backgroundView draws a white rectangle with round corners. cell.selectedBackgroundView draws a matching blue background which gets swapped in if the cell gets selected.

I’m going to show you a technique to steal what these are drawing and sizing it to our own needs. These are the steps:

  1. hook into willDisplayCellFor delegate method
  2. let the backgroundView draw itself into a bitmap context we provide
  3. make a UIImage from this bitmap context
  4. make a stretchable image out of that
  5. replace the backgroundView with a regular UIView
  6. add a new imageView to where the new border should be
  7. set all the autoresizing and contentStretch properties
  8. repeat 2 – 7 for the selectedBackgroundView

willDisplayCell is the second delegate I was referring to earlier. This is called right before the tableview cell actually gets displayed and after all internal modifications have taken place. This is the only and thus perfect spot to do our last second hijacking.

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
	// replace backgroundView if it's not our own
	if (indexPath.section == PARTNER_SECTION)
	{
		if ([cell.backgroundView class] != [UIView class])
		{
			UIGraphicsBeginImageContext(cell.backgroundView.bounds.size);
			[cell.backgroundView.layer renderInContext:UIGraphicsGetCurrentContext()];
			UIImage *backgroundImage = UIGraphicsGetImageFromCurrentImageContext();
			UIGraphicsEndImageContext();
			UIImage *shrunkenBackgroundImage = [backgroundImage stretchableImageWithHorizontalCapWith:20.0 verticalCapWith:round((backgroundImage.size.height - 1.0)/2.0)];
 
			UIView *replacementBackgroundView = [[UIView alloc] initWithFrame:cell.backgroundView.frame];
			replacementBackgroundView.autoresizesSubviews;
 
			UIImageView *indentedView = [[UIImageView alloc] initWithFrame:CGRectMake(PHOTO_INDENT, 0, replacementBackgroundView.bounds.size.width - PHOTO_INDENT,  replacementBackgroundView.bounds.size.height)];
			indentedView.image = shrunkenBackgroundImage;
			indentedView.contentMode = UIViewContentModeRedraw;
			indentedView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
			indentedView.contentStretch = CGRectMake(0.2, 0, 0.5, 1);
			[replacementBackgroundView addSubview:indentedView];
			[indentedView release];
 
			cell.backgroundView = replacementBackgroundView;
 
		}
 
		if ([cell.selectedBackgroundView class] != [UIView class])
		{
			UIGraphicsBeginImageContext(cell.selectedBackgroundView.bounds.size);
			[cell.selectedBackgroundView.layer renderInContext:UIGraphicsGetCurrentContext()];
			UIImage *backgroundImage = UIGraphicsGetImageFromCurrentImageContext();
			UIGraphicsEndImageContext();
			UIImage *shrunkenBackgroundImage = [backgroundImage stretchableImageWithHorizontalCapWith:20.0 verticalCapWith:round((backgroundImage.size.height - 1.0)/2.0)];
 
			UIView *replacementBackgroundView = [[UIView alloc] initWithFrame:cell.backgroundView.frame];
			replacementBackgroundView.autoresizesSubviews;
 
			UIImageView *indentedView = [[UIImageView alloc] initWithFrame:CGRectMake(PHOTO_INDENT, 0, replacementBackgroundView.bounds.size.width - PHOTO_INDENT,  replacementBackgroundView.bounds.size.height)];
			indentedView.image = shrunkenBackgroundImage;
			indentedView.contentMode = UIViewContentModeRedraw;
			indentedView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
			indentedView.contentStretch = CGRectMake(0.2, 0, 0.5, 1);
			[replacementBackgroundView addSubview:indentedView];
			[indentedView release];
 
			cell.selectedBackgroundView = replacementBackgroundView;
		}
 
		/*
		// naughty: we could add the photo directy to the tableView's content here
		CGRect frame = cell.frame;
		photoImageView.frame = CGRectMake(9.0, frame.origin.y, 64.0, 64.0);
		[tableView addSubview:photoImageView];
 
		// head sticker
		UIImageView *heartView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"heart.png"]];
		heartView.frame = CGRectMake(photoImageView.frame.origin.x + 40.0, photoImageView.frame.origin.y + 45.0, heartView.frame.size.width, heartView.frame.size.height);
		[tableView addSubview:heartView];
		[heartView release];
		*/
	}
}

I had to play around quite a bit with the contentStretch to prevent a skewing of the corners during sizing which you get if you rotate the iPhone or go into edit mode. But these values seem perfect.

Of course you also need the helper method to create our custom stretchable images from the captured backgrounds.

UIImage+Helpers.h

#import 
 
@interface UIImage (Helpers)
 
- (UIImage*)stretchableImageWithHorizontalCapWith:(CGFloat)horizontalCapWith verticalCapWith:(CGFloat)verticalCapWith;
 
@end

UIImage+Helpers.m

#import "UIImage+Helpers.h"
 
@implementation UIImage (Helpers)
- (UIImage*)stretchableImageWithHorizontalCapWith:(CGFloat)horizontalCapWith verticalCapWith:(CGFloat)verticalCapWith
{
	CGSize newSize = CGSizeMake(horizontalCapWith * 2.0 + 1.0, verticalCapWith * 2.0 + 1.0);
 
	UIGraphicsBeginImageContext(newSize);
	CGContextRef ctx = UIGraphicsGetCurrentContext();
 
	// upper left cap + 1 px middle
	CGContextSaveGState(ctx);
	CGContextAddRect(ctx, CGRectMake(0, 0, horizontalCapWith + 1.0, verticalCapWith + 1.0));
	CGContextClip(ctx);
	[self drawInRect:CGRectMake(0,0,self.size.width,self.size.height)];
	CGContextRestoreGState(ctx);
 
	// lower left cap
	CGContextSaveGState(ctx);
	CGContextAddRect(ctx, CGRectMake(0, verticalCapWith + 1.0, horizontalCapWith + 1.0, verticalCapWith));
	CGContextClip(ctx);
	[self drawInRect:CGRectMake(0,newSize.height - self.size.height,self.size.width,self.size.height)];
	CGContextRestoreGState(ctx);
 
	// upper right cap
	CGContextSaveGState(ctx);
	CGContextAddRect(ctx, CGRectMake(horizontalCapWith + 1.0, 0, horizontalCapWith, verticalCapWith+1.0));
	CGContextClip(ctx);
	[self drawInRect:CGRectMake(newSize.width - self.size.width,0,self.size.width,self.size.height)];
	CGContextRestoreGState(ctx);
 
	// lower right cap
	CGContextSaveGState(ctx);
	CGContextAddRect(ctx, CGRectMake(horizontalCapWith + 1.0, verticalCapWith + 1.0, horizontalCapWith, verticalCapWith));
	CGContextClip(ctx);
	[self drawInRect:CGRectMake(newSize.width - self.size.width,newSize.height - self.size.height,self.size.width,self.size.height)];
	CGContextRestoreGState(ctx);
 
	// static image
	UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
	UIGraphicsEndImageContext();
 
	return [newImage stretchableImageWithLeftCapWidth:horizontalCapWith topCapHeight:verticalCapWith];
}
 
@end

This method – implemented as category extension for UIImage – takes the outer left, top, right and bottom edges and 1 pixel in between and builds a minimal image to be used with the regular stretchableImageWithLeftCapWidth:topCapHeight:. So we end up with a perfectly stretchable background that looks just like the original, as long as we don’t stretch it vertically because this would destroy the gradient. Which we won’t.

With the imageView taking care of all the auto-resizing we can forget it from this point forward.

Until now we had to live with the standard backgrounds for grouped tableview cells. For DTAboutViewController I helped myself by using plain tableview cells and faked the grouped cell looks manually. With this knowledge here I can go back in and make it into a regular grouped tableview, selectively replacing the background where I like to.

I encourage you to not just copy/paste the above code but actually READ it because it can teach you a lot about the inner workings of table views and how to come up with a solution to achieving something that’s not provided in the SDK.


Categories: Recipes

%d bloggers like this: