Beginning Storyboards in iOS 5 Part 1
Update 10/24/12: If you’d like a new version of this tutorial fully updated for iOS 6 and Xcode 4.5, check out iOS 5 by Tutorials Second Edition! Note from Ray: This is the second iOS 5 tutorial in the iOS 5 Feast! This tutorial is a free preview chapter from our new book iOS 5 […] By Ray Wenderlich.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Beginning Storyboards in iOS 5 Part 1
45 mins
Designing Our Own Prototype Cells
Using a standard cell style is OK for many apps, but I want to add an image on the right-hand side of the cell that shows the player’s rating (in stars). Having an image view in that spot is not supported by the standard cell styles, so we’ll have to make a custom design.
Switch back to MainStoryboard.storyboard, select the prototype cell in the table view, and set its Style attribute to Custom. The default labels now disappear.
First make the cell a little taller. Either drag its handle at the bottom or change the Row Height value in the Size Inspector. I used the latter method to make the cell 55 points high.
Drag two Label objects from the Objects Library into the cell and place them roughly where the labels were previously. Just play with the font and colors and pick something you like. Do set the Highlighted color of both labels to white. That will look better when the user taps the cell and the cell background turns blue.
Drag an Image View into the cell and place it on the right, next to the disclosure indicator. Make it 81 points wide, the height isn’t very important. Set its Mode to Center (under View in the Attributes Inspector) so that whatever image we put into this view is not stretched.
I made the labels 210 points wide so they don’t overlap with the image view. The final design for the prototype cell looks something like this:
Because this is a custom designed cell, we can no longer use UITableViewCell’s textLabel and detailTextLabel properties to put text into the labels. These properties refer to labels that aren’t on our cell anymore. Instead, we will use tags to find the labels.
Give the Name label tag 100, the Game label tag 101, and the Image View tag 102. You can do this in the Attributes Inspector.
Then open PlayersViewController.m and change cellForRowAtIndexPath from PlayersViewController to:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:@"PlayerCell"];
Player *player = [self.players objectAtIndex:indexPath.row];
UILabel *nameLabel = (UILabel *)[cell viewWithTag:100];
nameLabel.text = player.name;
UILabel *gameLabel = (UILabel *)[cell viewWithTag:101];
gameLabel.text = player.name;
UIImageView * ratingImageView = (UIImageView *)
[cell viewWithTag:102];
ratingImageView.image = [self imageForRating:player.rating];
return cell;
}
This uses a new method, imageForRating. Add that method above cellForRowAtIndexPath:
- (UIImage *)imageForRating:(int)rating
{
switch (rating)
{
case 1: return [UIImage imageNamed:@"1StarSmall.png"];
case 2: return [UIImage imageNamed:@"2StarsSmall.png"];
case 3: return [UIImage imageNamed:@"3StarsSmall.png"];
case 4: return [UIImage imageNamed:@"4StarsSmall.png"];
case 5: return [UIImage imageNamed:@"5StarsSmall.png"];
}
return nil;
}
That should do it. Now run the app again.
Hmm, that doesn’t look quite right. We did change the height of the prototype cell but the table view doesn’t automatically take that into consideration. There are two ways to fix it: we can change the table view’s Row Height attribute or implement the heightForRowAtIndexPath method. The former is much easier, so let’s do that.
Note: You would use heightForRowAtIndexPath if you did not know the height of your cells in advance, or if different rows can have different heights.
Back in MainStoryboard.storyboard, in the Size Inspector of the Table View, set Row Height to 55:
By the way, if you changed the height of the cell by dragging its handle rather than typing in the value, then the table view’s Row Height property was automatically changed too. So it may have worked correctly for you the first time around.
If you run the app now, it looks a lot better!
Using a Subclass for the Prototype Cell
Our table view already works pretty well but I’m not a big fan of using tags to access the labels and other subviews of the prototype cell. It would be much more handy if we could connect these labels to outlets and then use the corresponding properties. As it turns out, we can.
Add a new file to the project, with the Objective-C class template. Name it PlayerCell and make it a subclass of UITableViewCell.
Change PlayerCell.h to:
@interface PlayerCell : UITableViewCell
@property (nonatomic, strong) IBOutlet UILabel *nameLabel;
@property (nonatomic, strong) IBOutlet UILabel *gameLabel;
@property (nonatomic, strong) IBOutlet UIImageView
*ratingImageView;
@end
Replace the contents of PlayerCell.m with:
#import "PlayerCell.h"
@implementation PlayerCell
@synthesize nameLabel;
@synthesize gameLabel;
@synthesize ratingImageView;
@end
The class itself doesn’t do much, it just adds properties for nameLabel, gameLabel and ratingImageView.
Back in MainStoryboard.storyboard, select the prototype cell and change its Class to “PlayerCell” on the Identity Inspector. Now whenever you ask the table view for a new cell with dequeueReusableCellWithIdentifier, it returns a PlayerCell instance instead of a regular UITableViewCell.
Note that I gave this class the same name as the reuse identifier — they’re both called PlayerCell — but that’s only because I like to keep things consistent. The class name and reuse identifier have nothing to do with each other, so you could name them differently if you wanted to.
Now you can connect the labels and the image view to these outlets. Either select the label and drag from its Connections Inspector to the table view cell, or do it the other way around, ctrl-drag from the table view cell back to the label:
Important: You should hook up the controls to the table view cell, not to the view controller! You see, whenever your data source asks the table view for a new cell with dequeueReusableCellWithIdentifier, the table view doesn’t give you the actual prototype cell but a *copy* (or one of the previous cells is recycled if possible). This means there will be more than one instance of PlayerCell at any given time. If you were to connect a label from the cell to an outlet on the view controller, then several copies of the label will try to use the same outlet. That’s just asking for trouble. (On the other hand, connecting the prototype cell to actions on the view controller is perfectly fine. You would do that if you had custom buttons or other UIControls on your cell.)
Now that we’ve hooked up the properties, we can simplify our data source code one more time:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
PlayerCell *cell = (PlayerCell *)[tableView
dequeueReusableCellWithIdentifier:@"PlayerCell"];
Player *player = [self.players objectAtIndex:indexPath.row];
cell.nameLabel.text = player.name;
cell.gameLabel.text = player.game;
cell.ratingImageView.image = [self
imageForRating:player.rating];
return cell;
}
That’s more like it. We now cast the object that we receive from dequeueReusableCellWithIdentifier to a PlayerCell, and then we can simply use the properties that are wired up to the labels and the image view. I really like how using prototype cells makes table views a whole lot less messy!
You’ll need to import the PlayerCell class to make this work:
#import "PlayerCell.h"
Run the app and try it out. When you run the app it should still look the same as before, but behind the scenes we’re now using our own table view cell subclass!
Here are some free design tips. There are a couple of things you need to take care of when you design your own table view cells. First off, you should set the highlighted color of the labels so that they look good then the user taps the row:
Second, you should make sure that the content you add is flexible so that when the table view cell resizes, the content sizes along with it. Cells will resize when you add the ability to delete or move rows, for example.
Add the following method to PlayersViewController.m:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete)
{
[self.players removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
When this method is present, swipe-to-delete is enabled on the table. Run the app and swipe a row to see what happens.
The Delete button slides into the cell but partially overlaps the stars image. What actually happens is that the cell resizes to make room for the Delete button, but the image view doesn’t follow along.
To fix this, open MainStoryBoard.storyboard, select the image view in the table view cell, and in the Size Inspector change the Autosizing so it sticks to its superview’s right edge:
Autosizing for the labels should be set up as follows, so they’ll shrink when the cell shrinks:
With those changes, the Delete button appears to push aside the stars:
You could also make the stars disappear altogether to make room for the Delete button, but that’s left as an exercise for the reader. The important point is that you should keep these details in mind when you design your own table view cells!