V2EX
   Tag:
 Example
Posted in iPhone on September 20th, 2008 by Xin

I will try to explain several important concepts of UITableView in this post, UITableView was the most confusing component when I was making my first iPhone app, it took me some time to understand it so that I’m really happy to share what I’ve learned with you.

Basics

UITableView is a subclass of UIView, that means you can initialize it and add it to a UIView using addToSubview method just like other UIView subclasses.

Following code will create an empty UITableView and add it to a view named mainView:

UITableView * aTableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, 320, 460)];
[mainView addToSubview:aTableView];

Now you’ve got an empty UITableView, but it can do nothing. For populating data and handling taps, you’ll need to set a datasource and a delegate for your UITableView. The object you’ll use for datasource and/or delegate must adopt protocol UITableViewDelegate and/or UITableViewDataSource. You don’t need to use two objects for handling the two protocols, one object is enough unless you have other reasons to use two.

Data Source

You can set an object as datasource like this:

[aTableView setDataSource:anObject];

The object acts as datasource usually needs to implement these methods:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView

This method will tell UITableView how many sections it will have. One example of sections is in Contacts app, contacts with different family names are organized into sections by initials.

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section

This method will tell UITableView what to display in section headers. In Contacts app, initials are displayed in section headers.

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

This method will tell UITableView how many cells it will have per section. If you have only one section, just simple return the total number of cells.

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

This is the most important method in datasource. After UITableView is created and has a datasource, when a cell is visible on screen, UITableView will query datasource for the cell to decide what to draw in the cell.

You have two ways to construct a cell, simple and complex.

Simple way:

This will produce a cell with a single line of text in standard font and size, as you have seen in Contacts app.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  static NSString *theIdentifier = @"theIdentifier";

  // Try to recover a cell from the table view with the given identifier, this is for performance
  UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:theIdentifier];

  // If no cell is available, create a new one using the given identifier
  if (cell == nil) {
    cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:MyIdentifier] autorelease];
  }

  // Fill the cell
  cell.text = [NSString stringWithString:@"Hello World"];
  return cell;
}

Complex way:

If you want to have images or labels in a cell, first you’ll need to subclass UITableViewCell, then you can draw them into a cell’s contentView. If you want to have customized backgrounds like in Twinkle, you can prepare a UIView and assign it to cell’s backgroundView.

When you have images or other customized content in contentView, you’ll usually need to use different identifiers for cells, this is different from the example in simple way. Unique identifiers usually come from data retrieved from Internet. For example, if you’re populating a UITableView with contents from RSS, you may use article ID as UITableViewCell identifier.

Below is a complete example of customized UITableViewCell, it uses a subclass ArticleCell inherits from UITableViewCell, ArticleCell has two UILabel (title, author) in properties. This example also demonstrated how to use a thread to download image from Internet and assign it to a UIImageView in UITableViewCell, you’ve seen this in two official apps: YouTube and App Store.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  Aricle * anArticle = [[articles objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];

  NSString * CellIdentifier = [anArticle articleID];
  ArticleCell * cell = (ArticleCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];

  if (cell == nil) {
    cell = [[[ArticleCell alloc] initWithFrame:CGRectMake(0,0,320,84) reuseIdentifier:CellIdentifier] autorelease];
    UIImageView * bgView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 320, 84)];
    UIImage * bgImage;
    if ((indexPath.row % 2) == 0) {
      bgImage = [UIImage imageNamed:@"cell-even.png"];
    } else {
      bgImage = [UIImage imageNamed:@"cell-odd.png"];
    }
    [bgView setImage:bgImage];
    [bgImage release];
    [cell setBackgroundView:bgView];
    [bgView release];
    cell.title.text = [anArticle title];
    cell.author.text = [anArticle author];
    [cell setRating:[anArticle ratingAverage]];
    if (anArticle.imageSmall) {
      [NSThread detachNewThreadSelector:@selector(downloadCover:) toTarget:cell withObject:[anArticle imageSmall]];
    }
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
  }
  return cell;
}

As of writing, customized cells do have some performance issues, scroll a list of customized cells is not smooth enough. Hope Apple would fix it in future.

Notice: cellForRowAtIndexPath method only executes when a cell is visible. And it can be executed multiple times when you scroll, so don’t do anything crazy in cellForRowAtIndexPath or it may crash the app easily.

Conclusion:

  • UITableView needs a datasource to guide it to draw cells.
  • Simple cells are filled by set text property.
  • Subclass UITableViewCell if you’re going to do complex drawing.
  • contentView and backgroundView are for complex drawing.
  • Don’t do expensive things in cellForRowAtIndexPath:.

Delegate

You can set an object as delegate like this:

[aTableView setDelegate:anObject];

So, now you know how to draw cells, but how to detect taps? This method in delegate object is for detecting taps:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

And there is another important method implemented by delegate:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath

This method will tell UITableView how tall a cell will be. If you go the simple way, you don’t need implement heightForRowAtIndexPath in delegate, a default height will be set automatically.

If each cell holds different text and you want to have variable height, please refer to a post I wrote before:

How to make UITableViewCell have variable height

Now we’ve covered two basic important things in UITableView: datasource and delegate. With knowledge you’ve learned from this post, it’s sufficient for building read only iPhone data clients, and there are more topics like dynamic add/remove and editing, I’ll cover these topics later. If you find any bugs, typos or memory leaks in my code, welcome to tell me in comments, I’ll be very glad to know.

Posted in iPhone on September 18th, 2008 by Xin

UIActionSheet is a very useful component missing from Interface Builder (as of writing, UIActionSheet is still missing from Interface Builder 3.1.1 of iPhone SDK 2.1), which means you can only add it programmatically.

You can create an instance of UIActionSheet like this:

UIActionSheet anActionSheet = [[UIActionSheet alloc] initWithTitle:@"Start a New Game" delegate:aDelegate cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil otherButtonTitles:@"Play Black", @"Play White", nil];

Its style can be configured:

anActionSheet.actionSheetStyle = UIActionSheetStyleBlackTranslucent;

From several choices:

  • UIActionSheetStyleAutomatic
  • UIActionSheetStyleDefault
  • UIActionSheetStyleBlackTranslucent
  • UIActionSheetStyleBlackOpaque

The delegate must adopt UIActionSheetDelegate protocol and implement this method to handle taps in UIActionSheet:

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex

And finally, you can make it show in a view by invoking showInView:

[anActionSheet showInView:aView]

UIActionSheet is used in many application, it’s great for prompting a list of options and let user choose one from the list. One example is Mail, when you tap reply, an UIActionSheet floats and let you choose from Reply / Reply All / Forward / Cancel.

Posted in iPhone on September 18th, 2008 by Xin

You have seen variable height UITableViewCell in iPhone applications like Twinkle, App Store. These applications have a UITableView as main part of UI, each cell in the table has a variable height according to text amount it holds.

So how to implement this in your own iPhone application?

After you created a UITableView, you will need to set a delegate and a datasource. There is a UITableViewDelegate method to tell UITableView how tall a cell would be:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath

So if you want UITableViewCell to have variable height like Twinkle, you’ll need to calculate the height of text block and return the result in method I mentioned above.

There are four NSString addition methods can do the calculations:

- (CGSize)sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size
- (CGSize)sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode
- (CGSize)sizeWithFont:(UIFont *)font forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode
- (CGSize)sizeWithFont:(UIFont *)font minFontSize:(CGFloat)minFontSize actualFontSize:(CGFloat *)actualFontSize forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode

To calculate the exact height of a text block, you’ll need to specify a large CGSize and those methods will return you exact size, here’s an example:

struct CGSize size;
size = [aString sizeWithFont:[UIFont systemFontOfSize:14] constrainedToSize:CGSizeMake(300.0, 4000) lineBreakMode:UILineBreakModeCharacterWrap];

Then you can get the height by accessing size.height.

As you’ve seen, UITableView in iPhone cannot generate dynamic height automatically, you’ll need to calculate the height yourself before you set the delegate. This is quite different if you’re coming from a HTML world.

If you’ve found a better way to implement this, please share with us in comments. Thanks.

p.s. Don’t forget to have some padding around text. And what’s why I use 300 as width in CGSizeMake, there are 10 pixels for padding in left and right side of text.