Ad

Our DNA is written in Swift
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

14 Comments »

  1. Hello Oliver,

    I have something very similar to this in my app Tap Forms Database. However, I implemented it in Interface Building with a UITableView that I sized appropriately.

    The only code I needed was the following:

    if (tableHeaderView == nil) {
    [[NSBundle mainBundle] loadNibNamed:@”FormEditHeaderView” owner:self options:nil];
    formTableView.tableHeaderView = tableHeaderView;
    }

    So yes, I used a table header view for this. But it’s way less code than what you outlined above and works just the same.

    I’m emailing you the screenshots of my view in IB.

    Thanks!

    Brendan

  2. Wow, thank you for this! I’ve been looking all over for it, you nailed it perfectly!

    Now Im just getting to warnings that don’t really get in the way, but just got me curious:

    In the helper class I get a “No ‘-renderInContext:’ method found”… know what its all about?

  3. you need to add the QuartzCore framework and add the header in the class file you have the warning in.

  4. Hi,

    First, I want to say thank you for posting this solution!!! You gave me light about this task, it took me a long time to find the solution.

    I have tried your code and suggestion but I somehow unable to get the CELL to be round corner like the way you have. The “other cell” cell are round but not the top one like the one next to the image. Other than getting the renderInContext warning, and I have added the QuatzCore framework but still get warning.

    Do you mind to send me this file you have done to see what I have miss?

    thanks so much!!!!!!!

  5. could it be that you have the table view in plain mode instead of grouped?

  6. Thanks for replying so quickly!!!!!

    I have it set to
    self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style : UITableViewStyleGrouped];

    at all time.

    Somehow, it is just a rectangle look and feel.

  7. Hi Oliver,

    This is my email address if you are willing to give me the code.
    Thanks again and again

    jake

    mountain5354@yahoo.com

  8. Hi,

    I have made it to work.

    Thanks!
    jake

  9. Congratulations, was is difficult or a simple mistake you made?

  10. Thanks,

    It was a simple fixed, and now I am learning something about the code. What I am trying to do is to do exactly like ipad or iphone CONTACTS App look and feel and as well as EDIT mode.

    thanks again!
    jake

  11. Hi,

    I am having a problem in the edit mode, I wonder if I can send you the file and see if you can help me to see why the CELL right next to the IMAGE do not show the CORNER but the rest “other value” do.

    thanks
    jake

    mountain5354@yahoo.com

  12. Years of service give you a perception of credibility and experience.

    Or if you give that facility to your customers,
    they will be very glad and will be an indirect source of your business booster.
    In case of an emergency, you must have the numbers of people whom you may call and they would be easily accessible.

  13. It is much easier to compare different web design service by reading online reviews.
    Many people wonder why it doesn’t just make sense to maintain a dedicated
    mobile site instead of redesigning the site with a responsive web
    design. Successful web sites are “user friendly,” allowing valuable information to be obtained
    easily by the user.

  14. There are a lot of well-experienced limo rentals that
    have been in business for quite a long time. You can make a great impact and a fantastic first impression if you are about to sign a big deal and want the client to
    have a stellar regard for your company. If you choose poorly
    you might be stranded on the side of the road or not be picked up at all.