Beginning Storyboards in iOS 5 Part 2
Note from Ray: This is the third iOS 5 tutorial in the iOS 5 Feast! This tutorial is a free preview chapter from our new book iOS 5 By Tutorials. Matthijs Hollemans wrote this chapter – the same guy who wrote the iOS Apprentice Series. Enjoy! This is a post by iOS Tutorial Team member […] 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 2
35 mins
The Game Picker Screen
Tapping the Game row in the Add Player screen should open a new screen that lets the user pick a game from a list. That means we’ll be adding yet another table view controller, although this time we’re going to push it on the navigation stack rather than show it modally.
Drag a new Table View Controller into the storyboard. Select the Game table view cell in the Add Player screen (be sure to select the entire cell, not one of the labels) and ctrl-drag to the new Table View Controller to create a segue between them. Make this a Push segue and give it the identifier “PickGame”.
Double-click the navigation bar and name this new scene “Choose Game”. Set the Style of the prototype cell to Basic, and give it the reuse identifier “GameCell”. That’s all we need to do for the design of this screen:
Add a new UITableViewController subclass file to the project and name it GamePickerViewController. Don’t forget to set the Class of the Table View Controller in the storyboard to link these two.
First we shall give this new screen some data to display. Add a new instance variable to GamePickerViewController.h:
@interface GamePickerViewController : UITableViewController {
NSArray * games;
}
Then switch to GamePickerViewController.m, and fill up this array in viewDidLoad:
- (void)viewDidLoad
{
[super viewDidLoad];
games = [NSArray arrayWithObjects:
@"Angry Birds",
@"Chess",
@"Russian Roulette",
@"Spin the Bottle",
@"Texas Hold’em Poker",
@"Tic-Tac-Toe",
nil];
}
Because we create this array in viewDidUnload, we have to release it in viewDidUnload:
- (void)viewDidUnload
{
[super viewDidUnload];
games = nil;
}
Even though viewDidUnload will never actually be called on this screen (we never obscure it with another view), it’s still good practice to always balance your allocations with releases.
Replace the data source methods from the template with:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
return [games count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:@"GameCell"];
cell.textLabel.text = [games objectAtIndex:indexPath.row];
return cell;
}
That should do it as far as the data source is concerned. Run the app and tap the Game row. The new Choose Game screen will slide into view. Tapping the rows won’t do anything yet, but because this screen is presented on the navigation stack you can always press the back button to return to the Add Player screen.
This is pretty cool, huh? We didn’t have to write any code to invoke this new screen. We just dragged from the static table view cell to the new scene and that was it. (Note that the table view delegate method didSelectRowAtIndexPath in PlayerDetailsViewController is still called when you tap the Game row, so make sure you don’t do anything there that will conflict with the segue.)
Of course, this new screen isn’t very useful if it doesn’t send any data back, so we’ll have to add a new delegate for that. Add the following to GamePickerViewController.h:
@class GamePickerViewController;
@protocol GamePickerViewControllerDelegate <NSObject>
- (void)gamePickerViewController:
(GamePickerViewController *)controller
didSelectGame:(NSString *)game;
@end
@interface GamePickerViewController : UITableViewController
@property (nonatomic, weak) id <GamePickerViewControllerDelegate> delegate;
@property (nonatomic, strong) NSString *game;
@end
We’ve added a delegate protocol with just one method, and a property that will hold the name of the currently selected game.
Change the top of GamePickerViewController.m to:
@implementation GamePickerViewController
{
NSArray *games;
NSUInteger selectedIndex;
}
@synthesize delegate;
@synthesize game;
This adds a new ivar, selectedIndex, and synthesizes the properties.
Then add the following line to the bottom of viewDidLoad:
selectedIndex = [games indexOfObject:self.game];
The name of the selected game will be set in self.game. Here we figure out what the index is for that game in the list of games. We’ll use that index to set a checkmark in the table view cell. For this work, self.game must be filled in before the view is loaded. That will be no problem because we’ll do this in the caller’s prepareForSegue, which takes place before viewDidLoad.
Change cellForRowAtIndexPath to:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:@"GameCell"];
cell.textLabel.text = [games objectAtIndex:indexPath.row];
if (indexPath.row == selectedIndex)
cell.accessoryType =
UITableViewCellAccessoryCheckmark;
else
cell.accessoryType = UITableViewCellAccessoryNone;
return cell;
}
This sets a checkmark on the cell that contains the name of the currently selected game. I’m sure this small gesture will be appreciated by the users of the app.
Replace the placeholder didSelectRowAtIndexPath method from the template with:
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
if (selectedIndex != NSNotFound)
{
UITableViewCell *cell = [tableView
cellForRowAtIndexPath:[NSIndexPath
indexPathForRow:selectedIndex inSection:0]];
cell.accessoryType = UITableViewCellAccessoryNone;
}
selectedIndex = indexPath.row;
UITableViewCell *cell =
[tableView cellForRowAtIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryCheckmark;
NSString *theGame = [games objectAtIndex:indexPath.row];
[self.delegate gamePickerViewController:self
didSelectGame:theGame];
}
First we deselect the row after it was tapped. That makes it fade from the blue highlight color back to the regular white. Then we remove the checkmark from the cell that was previously selected, and put it on the row that was just tapped. Finally, we return the name of the chosen game to the delegate.
Run the app now to test that this works. Tap the name of a game and its row will get a checkmark. Tap the name of another game and the checkmark moves along with it. The screen ought to close as soon as you tap a row but that doesn’t happen yet because we haven’t actually hooked up the delegate.
In PlayerDetailsViewController.h, add an import:
#import "GamePickerViewController.h"
And add the delegate protocol to the @interface line:
@interface PlayerDetailsViewController : UITableViewController <GamePickerViewControllerDelegate>
In PlayerDetailsViewController.m, add the prepareForSegue method:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:@"PickGame"])
{
GamePickerViewController *gamePickerViewController =
segue.destinationViewController;
gamePickerViewController.delegate = self;
gamePickerViewController.game = game;
}
}
This is similar to what we did before. This time the destination view controller is the game picker screen. Remember that this happens after GamePickerViewController is instantiated but before its view is loaded.
The “game” variable is new. This is a new instance variable:
@implementation PlayerDetailsViewController
{
NSString *game;
}
We use this ivar to remember the selected game so we can store it in the Player object later. We should give it a default value. The initWithCoder method is a good place for that:
- (id)initWithCoder:(NSCoder *)aDecoder
{
if ((self = [super initWithCoder:aDecoder]))
{
NSLog(@"init PlayerDetailsViewController");
game = @"Chess";
}
return self;
}
If you’ve worked with nibs before, then initWithCoder will be familiar. That part has stayed the same with storyboards; initWithCoder, awakeFromNib, and viewDidLoad are still the methods to use. You can think of a storyboard as a collection of nibs with additional information about the transitions and relationships between them. But the views and view controllers inside the storyboard are still encoded and decoded in the same way.
Change viewDidLoad to display the name of the game in the cell:
- (void)viewDidLoad
{
[super viewDidLoad];
self.detailLabel.text = game;
}
All that remains is to implement the delegate method:
#pragma mark - GamePickerViewControllerDelegate
- (void)gamePickerViewController:
(GamePickerViewController *)controller
didSelectGame:(NSString *)theGame
{
game = theGame;
self.detailLabel.text = game;
[self.navigationController popViewControllerAnimated:YES];
}
This is pretty straightforward, we put the name of the new game into our game ivar and the cell’s label, and then we close the Choose Game screen. Because it’s a push segue, we have to pop this screen off the navigation stack to close it.
Our done method can now put the name of the chosen game into the new Player object:
- (IBAction)done:(id)sender
{
Player *player = [[Player alloc] init];
player.name = self.nameTextField.text;
player.game = game;
player.rating = 1;
[self.delegate playerDetailsViewController:self didAddPlayer:player];
}
Awesome, we now have a functioning Choose Game screen!