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.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 5 of this article. Click here to view the first page.

Prototype cells

You may have noticed that ever since we added the Table View Controller, Xcode has been complaining:

Xcode warning: Prototype cells must have reuse identifiers

The warning message is, “Unsupported Configuration: Prototype table cells must have reuse identifiers”. When you add a Table View Controller to a storyboard, it wants to use prototype cells by default but we haven’t properly configured this yet, hence the warning.

Prototype cells are one of the cool advantages that storyboards offer over regular nibs. Previously, if you wanted to use a table view cell with a custom design you either had to add your subviews to the cell programmatically, or create a new nib specifically for that cell and then load it from the nib with some magic. That’s still possible, but prototype cells make things a lot easier for you. Now you can design your cells directly in the storyboard editor.

The Table View Controller comes with a blank prototype cell. Click on that cell to select it and in the Attributes Inspector set Style to Subtitle. This immediately changes the appearance of the cell to include two labels. If you’ve used table views before and created your own cells by hand, you may recognize this as the UITableViewCellStyleSubtitle style. With prototype cells you can either pick one of the built-in cell styles as we just did, or create your own custom design (which we’ll do shortly).

Creating a Prototype cell

Set the Accessory attribute to Disclosure Indicator and give the cell the Reuse Identifier “PlayerCell”. That will make Xcode shut up about the warning. All prototype cells are still regular UITableViewCell objects and therefore should have a reuse identifier. Xcode is just making sure we don’t forget (at least for those of us who pay attention to its warnings).

Run the app, and… nothing has changed. That’s not so strange, we still have to make a data source for the table so it will know what rows to display.

Add a new file to the project. Choose the UIViewController subclass template. Name the class PlayersViewController and make it a subclass of UITableViewController. The With XIB for user interface option should be unchecked because we already have the design of this view controller in the storyboard. No nibs today!

Creating a view controller with the table view controller template

Go back to the Storyboard Editor and select the Table View Controller. In the Identity Inspector, set its Class to PlayersViewController. That is the essential step for hooking up a scene from the storyboard with your own view controller subclass. Don’t forget this or your class won’t be used!

Setting the class name in the identity inspector

From now on when you run the app that table view controller from the storyboard is actually an instance of our PlayersViewController class.

Add a mutable array property to PlayersViewController.h:

#import <UIKit/UIKit.h>

@interface PlayersViewController : UITableViewController

@property (nonatomic, strong) NSMutableArray *players;

@end

This array will contain the main data model for our app. It contains Player objects. Let’s make that Player class right now. Add a new file to the project using the Objective-C class template. Name it Player, subclass of NSObject.

Change Player.h to the following:

@interface Player : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *game;
@property (nonatomic, assign) int rating;

@end

And change Player.m to:

#import "Player.h"

@implementation Player

@synthesize name;
@synthesize game;
@synthesize rating;

@end

There’s nothing special going on here. Player is simply a container object for these three properties: the name of the player, the game he’s playing, and a rating (1 to 5 stars).

We’ll make the array and some test Player objects in our App Delegate and then assign it to the PlayersViewController’s players property.

In AppDelegate.m, add an #import for the Player and PlayersViewController classes at the top of the file, and add a new instance variable named players:


#import "AppDelegate.h"
#import "Player.h"
#import "PlayersViewController.h"

@implementation AppDelegate {
	NSMutableArray *players;
}

// Rest of file...

Then change the didFinishLaunchingWithOptions method to:


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
	players = [NSMutableArray arrayWithCapacity:20];
	Player *player = [[Player alloc] init];
	player.name = @"Bill Evans";
	player.game = @"Tic-Tac-Toe";
	player.rating = 4;
	[players addObject:player];
	player = [[Player alloc] init];
	player.name = @"Oscar Peterson";
	player.game = @"Spin the Bottle";
	player.rating = 5;
	[players addObject:player];
	player = [[Player alloc] init];
	player.name = @"Dave Brubeck";
	player.game = @"Texas Hold’em Poker";
	player.rating = 2;
	[players addObject:player];
	UITabBarController *tabBarController = 
     (UITabBarController *)self.window.rootViewController;
	UINavigationController *navigationController = 
     [[tabBarController viewControllers] objectAtIndex:0];
	PlayersViewController *playersViewController = 
     [[navigationController viewControllers] objectAtIndex:0];
	playersViewController.players = players;
    return YES;
}

This simply creates some Player objects and adds them to the players array. But then it does the following:


UITabBarController *tabBarController = (UITabBarController *)
  self.window.rootViewController;
UINavigationController *navigationController = 
  [[tabBarController viewControllers] objectAtIndex:0];
PlayersViewController *playersViewController = 
  [[navigationController viewControllers] objectAtIndex:0];
playersViewController.players = players;

Yikes, what is that?! We want to assign the players array to the players property of PlayersViewController so it can use this array for its data source. But the app delegate doesn’t know anything about PlayersViewController yet, so it will have to dig through the storyboard to find it.

This is one of the limitations of storyboards that I find annoying. With Interface Builder you always had a reference to the App Delegate in your MainWindow.xib and you could make connections from your top-level view controllers to outlets on the App Delegate. That is currently not possible with storyboards. You cannot make references to the app delegate from your top-level view controllers. That’s unfortunate, but we can always get those references programmatically.

UITabBarController *tabBarController = (UITabBarController *)
  self.window.rootViewController;

We know that the storyboard’s initial view controller is a Tab Bar Controller, so we can look up the window’s rootViewController and cast it.

The PlayersViewController sits inside a navigation controller in the first tab, so we look up that UINavigationController object:

UINavigationController *navigationController = [[tabBarController 
  viewControllers] objectAtIndex:0];

and then ask it for its root view controller, which is the PlayersViewController that we are looking for:

PlayersViewController *playersViewController = 
  [[navigationController viewControllers] objectAtIndex:0];

Unfortunately, UINavigationController has no rootViewController property so we’ll have to delve into its viewControllers array. (It does have a topViewController property but that points to the top-most controller on the stack and we’re looking for the bottom-most one. At this point the app has just launched so technically we could have used topViewController, but that is not always the case.)

Now that we have an array full of Player objects, we can continue building the data source for PlayersViewController.

Open up PlayersViewController.m, and change the table view data source methods to the following:


- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
	return 1;
}

- (NSInteger)tableView:(UITableView *)tableView 
  numberOfRowsInSection:(NSInteger)section
{
	return [self.players count];
}

The real work happens in cellForRowAtIndexPath. The version from the Xcode template looks like this:


- (UITableViewCell *)tableView:(UITableView *)tableView 
  cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    
    UITableViewCell *cell = [tableView 
      dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] 
          initWithStyle:UITableViewCellStyleDefault 
          reuseIdentifier:CellIdentifier];
    }
    
    // Configure the cell...
    return cell;
}

That is no doubt how you’ve been writing your own table view code all this time. Well, no longer! Replace that method with:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	UITableViewCell *cell = [tableView 
      dequeueReusableCellWithIdentifier:@"PlayerCell"];
	Player *player = [self.players objectAtIndex:indexPath.row];
	cell.textLabel.text = player.name;
	cell.detailTextLabel.text = player.game;
    return cell;
}

That looks a lot simpler! The only thing you need to do to get a new cell is:

UITableViewCell *cell = [tableView 
  dequeueReusableCellWithIdentifier:@"PlayerCell"];

If there is no existing cell that can be recycled, this will automatically make a new copy of the prototype cell and return it to you. All you need to do is supply the reuse identifier that you set on the prototype cell in the storyboard editor, in our case “PlayerCell”. Don’t forget to set that identifier, or this little scheme won’t work!

Because this class doesn’t know anything about the Player object yet, it needs an #import at the top of the file:

#import "Player.h"

And we should not forget to synthesize the property that we added earlier:

@synthesize players;

Now you can run the app, and lo and behold, the table view has players in it:

Table view with data

Note: In this app we’re using only one prototype cell but if your table needs to display different kinds of cells then you can simply add additional prototype cells to the storyboard. You can either duplicate the existing cell to make a new one, or increment the value of the Table View’s Prototype Cells attribute. Be sure to give each cell its own re-use identifier, though.

It just takes one line of code to use these newfangled prototype cells. I think that’s just great!

Contributors

Over 300 content creators. Join our team.