Matt's Blog

Three20 TTTableItem Tutorial


Update: This tutorial is outdated, proceed at your own risk.

Say Hello to Three20 TTTableItemCells

Over the last couple of months I’ve shown you how to use Joe Hewitt’s Three20 iPhone library to create css-like stylesheets for your iPhone Apps, as well as custom table cells for your iPhone Apps.

But a while ago Joe changed up the way we make custom table cells. Instead of using TTTableField and TTTableFieldCell we now use TTTableItem and TTTableItemCell to accomplish custom table cells.

So today I am going to show you how to create an iPhone App using Three20′s TTTableItem and TTTableItemCell.

Lets start the show

Here’s our game plan for creating our custom cells:

  1. Setup our App, create some necessary files, etc
  2. Make our own TableItems by subclassing TTTableItem
  3. Make our own TableItemCells by subclassing TTTableItemCell
  4. Instantiate our new TableItems in our DataSource
  5. Make the App render the correct TableItemCell for each TableItem we just made
  6. Finally when we’ve done all the other stuff, we’ll have a sexy new app with sexy custom table cells

1. Setup the App

I think the easiest way to learn how to do this is too follow along step by step with this tutorial (i’ve created a three20 App template that works great for this)

  1. Download or Clone my Three20 Tutorial repository from github
  2. When my repo is downloaded or cloned open it and copy and past the basic-navigation-app-template somewhere else on your computer (if you want)The three20-iPhone-tutorials folder should now look like this:
  3. Picture 14
  4. Rename basic-navigation-app-template copy folder to something like tableitem-tutorial-working
  5. Open the Xcode Project file inside of your new folder (again we’re using a template App that I created)
  6. Now we need to create some new classes inside of XCode:
    1. We need a new class that will subclass TTTableItem. Go File >> New File >> Objective-C Class >> Name it BNTableItem (following Joe’s naming conventions). Make sure “Also create BNTableItem.h” is checked.
    2. We also need a new class where we will sublclass TTTableItemCell. Go File >> New File >> Objective-C Class >> Name it BNTableItemCell (following Joe’s naming conventions). Make sure “Also create BNTableItemCell.h” is checked.
  7. Now that our all of our files are created let’s give our App a title. Go into RootViewController.mand change the lineself.title = @”Tutorial Title”;toself.title = @”TableItem Tutorial”;.RootViewController.mshould now look like this:
    #import "RootViewController.h"
    
    #pragma mark import dataSource
    #import "RootViewDataSource.h"
    
    # pragma mark import table cells
    
    @implementation RootViewController
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // UIViewController
    
    - (void)loadView {
        [super loadView];
    
        self.tableView = [[[UITableView alloc] initWithFrame:self.view.bounds
                                                       style:UITableViewStyleGrouped] autorelease];
        self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth
        | UIViewAutoresizingFlexibleHeight;
        self.variableHeightRows = YES;
        self.title = @"TableItem Tutorial";
        [self.view addSubview:self.tableView];
    }
    
    //////////////////////////////////////////////////////////////////////////////////////////////////
    // TTTableViewController
    
    - (id)createDataSource {
        return [RootViewDataSource rootViewDataSource];
    }
    
    @end
  8. Make sure that you have selected Device 3.0 or Simulator 3.0 to run the app
  9. Open the App and see what happens. Click Build and Go (or Build >> Build and Go (Run)).The App should look like this (nothing special yet, but just you wait):
  10. Picture 1

2. Subclass TTTableItem

Now we need to make our TableItem. To do this we are going to subclass TTTableCaptionedItem.

TableItems hold the data, TableItemCells actually display the data.

Sink that into your head, it’s important:

TableItems holds the data, TableItemCells display.

Do the following steps, once they are done I will explain what was accomplished.

  1. Go into BNTableItem.h and delete everything
  2. Go into BNTableItem.m and delete everything
  3. Now we need to subclass TTTableCaptionedItem because it’s quite similar to what we are trying to accomplish, but needs some enhancements. Copy and paste this code into the empty BNTableItem.hfile:
    #import "Three20/Three20.h"
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    ////////////////////////////////////////////////////////
    //////   BNTableCaptionedItemWithThreeImagesBelow  /////
    ////////////////////////////////////////////////////////
    
    @interface BNTableCaptionedItemWithThreeImagesBelow : TTTableCaptionedItem {
    
    }
    
    @end
  4. Now copy and paste this code into BNTableItem.m:
    #import "BNTableItem.h"
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    ////////////////////////////////////////////////////////
    //////   BNTableCaptionedItemWithThreeImagesBelow  /////
    ////////////////////////////////////////////////////////
    
    @implementation BNTableCaptionedItemWithThreeImagesBelow
    
    @end

We just created a subclass of  TTTableCaptionedItem which we named BNTableCaptionedItemWithThreeImagesBelow.

But we haven’t changed anything in our class yet, let’s do that now:

  1. Go back to BNTableItem.hand modify it so the file looks exactly like this:
    #import "Three20/Three20.h"
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    ////////////////////////////////////////////////////////
    //////   BNTableCaptionedItemWithThreeImagesBelow  /////
    ////////////////////////////////////////////////////////
    
    @interface BNTableCaptionedItemWithThreeImagesBelow : TTTableCaptionedItem {
    	NSString* _image1;
    	NSString* _image2;
    	NSString* _image3;
    	TTStyle* _imageStyle;
    }
    
    @property(nonatomic,copy) NSString* image1;
    @property(nonatomic,copy) NSString* image2;
    @property(nonatomic,copy) NSString* image3;
    @property(nonatomic,retain) TTStyle* imageStyle;
    
    + (id)itemWithText:(NSString*)text caption:(NSString*)caption image1:(NSString*)image1 image2:(NSString*)image2 image3:(NSString*)image3;
    
    @end
  2. Go to back to BNTableItem.m and modify itso the file looks exactly like this:
    #import "BNTableItem.h"
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    ////////////////////////////////////////////////////////
    //////   BNTableCaptionedItemWithThreeImagesBelow  /////
    ////////////////////////////////////////////////////////
    
    @implementation BNTableCaptionedItemWithThreeImagesBelow
    @synthesize image1 = _image1, image2 = _image2, image3 = _image3, imageStyle = _imageStyle;
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // class public
    
    + (id)itemWithText:(NSString*)text caption:(NSString*)caption image1:(NSString*)image1 image2:(NSString*)image2 image3:(NSString*)image3 {
    		BNTableCaptionedItemWithThreeImagesBelow* item = [[[self alloc] init] autorelease];
    		item.text = text;
    		item.caption = caption;
    		item.image1 = image1;
    		item.image2 = image2;
    		item.image3 = image3;
    		return item;
    	}
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // NSObject
    
    - (id)init {
    	if (self = [super init]) {
    		_image1 = nil;
    		_image2 = nil;
    		_image3 = nil;
    		_imageStyle = nil;
    	}
    	return self;
    }
    
    - (void)dealloc {
    	TT_RELEASE_MEMBER(_image1);
    	TT_RELEASE_MEMBER(_image2);
    	TT_RELEASE_MEMBER(_image3);
    	TT_RELEASE_MEMBER(_imageStyle);
    	[super dealloc];
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // NSCoding
    
    - (id)initWithCoder:(NSCoder*)decoder {
    	if (self = [super initWithCoder:decoder]) {
    		self.image1 = [decoder decodeObjectForKey:@"image1"];
    		self.image2 = [decoder decodeObjectForKey:@"image2"];
    		self.image3 = [decoder decodeObjectForKey:@"image3"];
    	}
    	return self;
    }
    
    - (void)encodeWithCoder:(NSCoder*)encoder {
    	[super encodeWithCoder:encoder];
    	if (self.image1) {
    		[encoder encodeObject:self.image1 forKey:@"image1"];
    	}
    	if (self.image2) {
    		[encoder encodeObject:self.image2 forKey:@"image2"];
    	}
    	if (self.image3) {
    		[encoder encodeObject:self.image3 forKey:@"image3"];
    	}
    }
    
    @end

Finished? Awesome. I am not going to explain everything that we did there, but I will give an overview.

  • We added 3 new properties to our class (one for each image)
  • We made a method called + (id)itemWithText:(NSString*)text caption:(NSString*)caption image1:(NSString*)image1 image2:(NSString*)image2 image3:) which accepts the addresses for each of the 3 images as well as our text content
  • We added the 3 image properties to the init method, the initWithCoder method, and encodeWithCoder method.

The BNTableItem part of this tutorial is now finished.

The next part of our tutorial deals with displaying the data from our TableItem (the labels, images, etc), lets get started

3. Subclass TTTableItemCell

Subclassing TTTableCaptionedItemCell (itself a subclass of TTTableItemCell) will be similar to the subclassing we just did of TTTableCaptionedItem.

  1. Go into BNTableItemCell.h and delete everything
  2. Go into BNTableItemCell.m and delete everything
  3. We should now have NOTHING in either BNTableItemCell.h or BNTableItemCell.m
  4. Now subclass TTTableCaptionedItemCell. Go into and BNTableItemCell.hand make it look like this:
    #import "Three20/Three20.h"
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    ////////////////////////////////////////////////////////////
    //////   BNTableCaptionedItemWithThreeImagesBelowCell     //
    ////////////////////////////////////////////////////////////
    
    @interface BNTableCaptionedItemWithThreeImagesBelowCell : TTTableCaptionedItemCell {
      TTImageView* _imageView1;
      TTImageView* _imageView2;
      TTImageView* _imageView3;
    }
    
    @end
  5. Then go into BNTableItemCell.mand make it look like this:
    #import "BNTableItemCell.h"
    #import "BNTableItem.h"
    #import "BNDefaultStylesheet.h"
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    static CGFloat kHPadding = 10;
    static CGFloat kVPadding = 15;
    static CGFloat kImageWidth = 80;
    static CGFloat kImageHeight = 80;
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    //////////////////////////////////////////////////////////////
    //////   BNTableCaptionedItemWithThreeImagesBelowCell     ////
    //////////////////////////////////////////////////////////////
    
    @implementation BNTableCaptionedItemWithThreeImagesBelowCell
    
    + (CGFloat)tableView:(UITableView*)tableView rowHeightForItem:(id)item {
    	BNTableCaptionedItemWithThreeImagesBelow* captionedItem = item;
    
    	CGFloat maxWidth = tableView.width - kHPadding*2;
    
    	CGSize textSize = [captionedItem.text sizeWithFont:TTSTYLEVAR(myHeadingFont)
    					   constrainedToSize:CGSizeMake(maxWidth, CGFLOAT_MAX)
    					   lineBreakMode:UILineBreakModeWordWrap];
    	CGSize subtextSize = [captionedItem.caption sizeWithFont:TTSTYLEVAR(mySubtextFont)
    						  constrainedToSize:CGSizeMake(maxWidth, CGFLOAT_MAX) lineBreakMode:UILineBreakModeWordWrap];
    
    	return kVPadding*2 + textSize.height + subtextSize.height + kImageHeight + kVPadding;
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString*)identifier {
    	if (self = [super initWithStyle:UITableViewCellStyleValue2 reuseIdentifier:identifier]) {
    		_item = nil;
    
    		_imageView1 = [[TTImageView alloc] initWithFrame:CGRectZero];
    		[self.contentView addSubview:_imageView1];
    
    		_imageView2 = [[TTImageView alloc] initWithFrame:CGRectZero];
    		[self.contentView addSubview:_imageView2];
    
    		_imageView3 = [[TTImageView alloc] initWithFrame:CGRectZero];
    		[self.contentView addSubview:_imageView3];
    
    	}
    	return self;
    }
    
    - (void)dealloc {
    	TT_RELEASE_MEMBER(_imageView1);
    	TT_RELEASE_MEMBER(_imageView2);
    	TT_RELEASE_MEMBER(_imageView3);
    	[super dealloc];
    }
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // UIView
    
    - (void)layoutSubviews {
    	[super layoutSubviews];
    
    	[self.detailTextLabel sizeToFit];
    	self.detailTextLabel.top = kVPadding;
    
    	self.textLabel.height = self.detailTextLabel.height;
    
    	_imageView1.frame = CGRectMake(20, self.detailTextLabel.bottom + kVPadding, kImageWidth, kImageHeight);
    	_imageView2.frame = CGRectMake(_imageView1.right + kHPadding, self.detailTextLabel.bottom + kVPadding, kImageWidth, kImageHeight);
    	_imageView3.frame = CGRectMake(_imageView2.right + kHPadding, self.detailTextLabel.bottom + kVPadding, kImageWidth, kImageHeight);
    
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // TTTableViewCell
    
    - (id)object {
    	return _item;
    }
    
    - (void)setObject:(id)object {
    	if (_item != object) {
    		[super setObject:object];
    
    		BNTableCaptionedItemWithThreeImagesBelow* item = object;
    
    		self.textLabel.textColor = TTSTYLEVAR(myHeadingColor);
    		self.textLabel.font = TTSTYLEVAR(myHeadingFont);
    		self.textLabel.textAlignment = UITextAlignmentRight;
    		self.textLabel.contentMode = UIViewContentModeCenter;
    		self.textLabel.lineBreakMode = UILineBreakModeWordWrap;
    		self.textLabel.numberOfLines = 0;
    
    		self.detailTextLabel.textColor = TTSTYLEVAR(mySubtextColor);
    		self.detailTextLabel.font = TTSTYLEVAR(mySubtextFont);
    		self.detailTextLabel.textAlignment = UITextAlignmentLeft;
    		self.detailTextLabel.contentMode = UIViewContentModeTop;
    		self.detailTextLabel.lineBreakMode = UILineBreakModeWordWrap;
    
    		_imageView1.URL = item.image1;
    		_imageView1.style = item.imageStyle;
    
    		_imageView2.URL = item.image2;
    		_imageView2.style = item.imageStyle;
    
    		_imageView3.URL = item.image3;
    		_imageView3.style = item.imageStyle;
    
        }
    }
    
    @end

Awesome, we’re on a roll, we just subclassed TTTableCaptionedItemCell, then modified it so that it has 3 image views underneath the heading and caption.

  • In the rowHeightForItem method we made our cell adjust it’s size to the text content AND our new images.
  • In the initWithStyle method we allocated 3 TTImageViews (to hold our images) and added the TTImageViews as subviews of our main view.
  • in the layoutSubviews method we  made the textLabel (the heading label) and the detailTextLabel (the subtext label) fit together nicely, as well as sizing all three images and making them fit nice and centered underneath the text.
  • Finally in the setObject method we set the textColor, the font, and a few other important properties of our textLabel, and detailTextLabel, and we gave each of our 3 TTImageViews the URL of the image their supposed to display (the URL of each TTImageView should match the corresponding URL given for each address we are fed to our TTTableItem).

Now we need to put our feed our TableItems to our TableView datasource

4. Add Our TableItems to the DataSource

Now what we’re going to do is instantiate and feed BNTableCaptionedItemWithThreeImagesBelow into our DataSource a few times while feeding it some text content and image addresses.

  1. To instantiate and feed our Tableitem into our datasource (twice), make RootViewDataSource.mlook exactly like this:
    #import "RootViewDataSource.h"
    #import "BNTableItem.h"
    #import "BNTableItemCell.h"
    
    @implementation RootViewDataSource
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // public
    
    + (RootViewDataSource*)rootViewDataSource {
    	RootViewDataSource* dataSource =  [[[RootViewDataSource alloc] initWithItems:
    										[NSMutableArray arrayWithObjects: [BNTableCaptionedItemWithThreeImagesBelow itemWithText:@"These are some pictures of me (Matt) doing different stuff"
    																									caption:@"Matt"
    																									image1:@"http://mattvague.com/wordpress/wp-content/uploads/2009/08/boating.jpg"
    																									image2:@"http://mattvague.com/wordpress/wp-content/uploads/2009/08/cooking.jpg"
    																									image3:@"http://mattvague.com/wordpress/wp-content/uploads/2009/08/surfing.jpg"],
    																		 [BNTableCaptionedItemWithThreeImagesBelow itemWithText:@"These are some pictures of Vancouver, BC"
    																									caption:@"VanCity"
    																									image1:@"http://mattvague.com/wordpress/wp-content/uploads/2009/08/Vancouver_Aerial.jpg"
    																									image2:@"http://mattvague.com/wordpress/wp-content/uploads/2009/08/Coast-Mountains-BC.jpg"
    																									image3:@"http://mattvague.com/wordpress/wp-content/uploads/2009/08/Vancouver_Aerial_2.jpg"],
    																		 nil]] autorelease];
    	return dataSource;
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    - (void)dealloc {
    	[super dealloc];
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // TTTableViewDataSource
    
    - (Class)tableView:(UITableView*)tableView cellClassForObject:(id) object {
    
    	if ([object isKindOfClass:[BNTableCaptionedItemWithThreeImagesBelow class]]) {
    		return [BNTableCaptionedItemWithThreeImagesBelowCell class];
    	} else {
    		return [super tableView:tableView cellClassForObject:object];
    	}
    }
    
    - (void)tableView:(UITableView*)tableView prepareCell:(UITableViewCell*)cell
    forRowAtIndexPath:(NSIndexPath*)indexPath {
    	cell.accessoryType = UITableViewCellAccessoryNone;
    }
    
    @end
  2. Now try launching the App again.

Looks kinda weird doesn’t it? That’s because we need to do one last thing which is to tell our App which kind of TableItemCell to render for our TableItems.

Right now the App is just rendering the closest matching TableItemCell it could find, but not the one that we want (which is the one we just created).

5. Render the correct TableItemCell for our TableItems

This part is very simple, and I probably could have combined it with the last step, but it is something that is quite vital and I think deserves its own step.

  1. Add the following code to RootViewDataSource.m, underneath the rootViewDataSource, and dealloc methods:
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // TTTableViewDataSource
    
    - (Class)tableView:(UITableView*)tableView cellClassForObject:(id) object {
    
    	if ([object isKindOfClass:[BNTableCaptionedItemWithThreeImagesBelow class]]) {
    		return [BNTableCaptionedItemWithThreeImagesBelowCell class];
    	} else {
    		return [super tableView:tableView cellClassForObject:object];
    	}
    }
  2. Now RootViewDataSource.mshould look exactly like this:
    #import "RootViewDataSource.h"
    #import "BNTableItem.h"
    #import "BNTableItemCell.h"
    
    @implementation RootViewDataSource
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // public
    
    + (RootViewDataSource*)rootViewDataSource {
    	RootViewDataSource* dataSource =  [[[RootViewDataSource alloc] initWithItems:
    										[NSMutableArray arrayWithObjects: [[BNTableCaptionedItemWithThreeImagesBelow itemWithText:@"These are some pictures of me (Matt) doing different stuff"
    																									caption:@"Matt"
    																									image1:@"http://mattvague.com/wordpress/wp-content/uploads/2009/08/boating.jpg"
    																									image2:@"http://mattvague.com/wordpress/wp-content/uploads/2009/08/cooking.jpg"
    																									image3:@"http://mattvague.com/wordpress/wp-content/uploads/2009/08/surfing.jpg"] autorelease],
    																		 [[BNTableCaptionedItemWithThreeImagesBelow itemWithText:@"These are some pictures of Vancouver, BC"
    																									caption:@"VanCity"
    																									image1:@"http://mattvague.com/wordpress/wp-content/uploads/2009/08/Vancouver_Aerial.jpg"
    																									image2:@"http://mattvague.com/wordpress/wp-content/uploads/2009/08/Coast-Mountains-BC.jpg"
    																									image3:@"http://mattvague.com/wordpress/wp-content/uploads/2009/08/Vancouver_Aerial_2.jpg"] autorelease]
    																			,nil]] autorelease];
    	return dataSource;
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    - (void)dealloc {
    	[super dealloc];
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // TTTableViewDataSource
    
    - (Class)tableView:(UITableView*)tableView cellClassForObject:(id) object {
    
    	if ([object isKindOfClass:[BNTableCaptionedItemWithThreeImagesBelow class]]) {
    		return [BNTableCaptionedItemWithThreeImagesBelowCell class];
    	} else {
    		return [super tableView:tableView cellClassForObject:object];
    	}
    }
    
    - (void)tableView:(UITableView*)tableView prepareCell:(UITableViewCell*)cell
    forRowAtIndexPath:(NSIndexPath*)indexPath {
    	cell.accessoryType = UITableViewCellAccessoryNone;
    }
    
    @end

6. Run the App

Run our App by clicking on “Build and Go”

It should look like this:

Picture 4

Download the working version

If you are having trouble or just want to checkout the source code, the final working version of my tutorial is availbile on github. Download or clone my repo and then open the tableitem-tutorial folder and open its Xcode Project.

Posted on - Archive

My name is Matt Vague and I'm a Front End Developer from Vancouver, Canada. I'm passionate about beautiful, responsive user interfaces and I'm a Front End-Developer at VersaPay, core team member of ActiveAdmin and the creator of BC Liquor Locator.