CPSC 91 Spring 2011: Lab 04


Introduction

In this week's lab, you will implement a table view to display a list of cards from the game Race for the Galaxy. There's absolutely no need to understand how to play this game in order to complete the lab! If you really want, you download an open source version of the game.

Like last week's lab, your code:

The cards, the model, and loading the data

Race for the Galaxy (RFTG) is played with a set of special cards. Each card has a number of features associated with it including the name of the card, the cost to place the card in the game, the type of card, etc. You will not use most of the data on the card for this lab, though you are welcome to expand the solution to include the other features.

I am providing you with three important files: RFTGCard.h, RFTGCard.m and RFTG.zip. RFTGCard.h and RFTGCard.m are the interface and implementation of a class that stores all of the data that you will find on a single card. The class provides properties to access all of the various features on the card. In addition, there is a class method called +cardListFromBundle that will load the cards out of your Resources folder and return all of the cards to you in an NSArray. In order to access that data, you will need to unzip the RFTG.zip file and add the contents (cards.data, Icon.png, and a folder called images/) to your Resources folder. Be sure to copy the data to your project. The Icon.png is only needed if you want an icon for your app when you install it on your iDevice.

The cards.data contains an array of all of the RFTGCards that you need. You will not modify, insert, or delete cards during the lab in any way. The images/ folder contains images for 191 different cards, with filenames like "card090.jpg". In the RFTGCard class, there is a property called imageNumber. For card090.jpg, the imageNumber is 90. Note: The card images are copyrighted by the manufacturer of the game. To prevent wide distribution of these images, you must be on the Swarthmore network to access them.

In addition to the name of the card (cardTitle) and the imageNumber for the card, you also need to know what expansion set the card is from. There was an original game (the "base" set), then there were three expansions (for the purposes of the lab, we'll call them "1", "2" and "3"). You'll notice that there is a property in RFTGCard called numberInSet which is an NSArray. The array numberInSet has 4 NSNumbers in it describing the number of cards in each of the Base set plus the three expansions. Many cards have (1,1,1,1) as the value for the array, meaning that one instance of this card exists in each version of the game. Another example is (0,2,2,2) which means that the card appeared 0 times in the base set, but twice in expansions 1, 2 and 3. There are many other variants. (Note: There are two cards in the full set with the same name but different images. One image is valid for (1,1,0,0) and the other image is valid for (0,0,1,1). It doesn't require anything special to handle it, but you should be aware of it.)

A high-level description

In this app, you're going to make a table view that lists the names of all of the cards in the game, regardless of which expansion set they were in. You saw in class yesterday how to make a table and you'll do the same thing here, with one important difference. For the lab, you're going to implement sections. The sections will separate cards that start with the letter A from those that start with the letter B, etc. You'll also add an index to the side of the view so that users can quickly jump to a different part of the table.

Once you get that working, there are a few required pieces and then lots of optional pieces.

The first required part is that you're going to want the user to be able to select a row in the table and the new view that gets pushed on the UINavigationController (see the StudentManager project) will show the image of the card. I'll give you some code below to help out with that.

The second required part of the lab is that you need to add a configuration screen that allows the user to only see cards from a single expansion set. You are welcome to add additional filters, but you need to add this one. In the StudentManager app, we made an Add button (UIBarButtonSystemItemAdd) that pushed on a new view controller, allowing us to type in details about a new student. In the lab, you're going to add a UIBarButtonSystemItemAction that will push on a new view allowing you to select (using a UISegmentedControl) which expansion you want to use. The control should let you select either "All" (all cards, regardless of set), and "Base", "1", "2", and "3" which will only show cards from those particular expansion sets. When you go back to the table view from the configuration screen, the table should only list the cards matching the configuration.

Optionally, there are a few extensions things you could do, from allowing the user to flip through the images once that view is loaded (using a UISwipeGestureRecognizer), to adding additional filters on the other features, to putting the images in a ScrollView so you can pinch and zoom them, to simply saving the user's preference for which card set they want to see when they load the app each time.

Details

You're going to want to start with a Window-Based App. Your AppDelegate needs to have some code in -application:didFinishLaunchingWithOptions: that adds the table view to the window. Remember from class yesterday that you're going to want to put the table view controller into a navigational controller, then add the navigation controller to the window. Once you've done that, you shouldn't need to make any changes to the AppDelegate.

You should also add the files I mentioned earlier in your project: RFTGCard.h and RFTGCard.m in your Classes directory, and the cards.data, images/ directory, the Icon.png in the Resources directory.

To these files, you're going to eventually add three view controllers. The first view controller, CardListTableViewController, is the top level view controller for your table. You'll create that by adding a new File, subclass of UIViewController, being sure to check UITableViewSubclass, but do not include a XIB file. The second view controller, CardIamgeViewController, is the one that will appear when the user clicks on a row. This view controller will have a very simple view whose entire space is taken up by a UIImageView. It's straightforward (perhaps even easier) to do this without a XIB file, but you're welcome to use one for this if you want. For the final view controler, ExpansionViewController, you'll want to create a XIB file and put the UISegmentedControl into the view.

Begin with CardListTableViewController. The table view controller will point to the model (simply an NSArray* of cards). You should make this be a property, of course. Here's sample code for the getter of that property.

- (NSArray *)cards
{
   if (!cards) {
     //use the property to ensure this is retained
     self.cards = [[RFTGCard cardListFromBundle] 
           sortedArrayUsingSelector:@selector(compare:)];
   }
   return cards;
}
Notice that this uses the RFTGCard class method to load all of the data (from the cards.data file in the resources directory). It also sorts all of the data which you'll need in order to get sections working properly.

In class, our table had only one section, so we only needed to implement these datasource methods:

-numberOfSectionsInTableView:
-tableView:numberOfRowsInSection:
-tableView:cellForRowAtIndexPath:;
Since we're not doing any editing, we don't need the methods that handle movement and editing. However, since we want to implement sections, we'll need to implement:
-tableView:titleForHeaderInSection:
-sectionIndexTitlesForTableView: 
-tableView:sectionForSectionIndexTitle:atIndex:
The first method is used to describe the text that goes in the header that appears above each section in the table. The second two methods are used to get the index on the side of the view working. The index is a bit tricky, so you should definitely not worry about getting that working until you're sure you've got other things working well. The first of the section index methods returns an NSArray of all the section names. The second of the section index methods returns which section number corresponds to the letter in the section name so that the table view knows where to go when you press a letter in the index. You should read the documentation on those methods to see how they work. Importantly, your table must be UITableViewStylePlain not UITableViewStyleGrouped the way we had it in the StudentManager demo.

For simplicity, you might want to start having only 1 section (like we did with the StudentManager) and then add support for multiple sections once things are up and running.

Once you've got the data appearing in the table with sections, you can work on what happens when -tableView:didSelectRowAtIndexPath: is called. You're going to want to push a CardImageViewController onto the navigation controller to view the image of the card. Although you are welcome to implement this differently, I will assume that you will have a property called "card" in CardImageViewController which is a pointer to the card it is viewing (similar to what we did in class with editStudent in StudentDetailViewController). From that card property, you can get the imageNumber and then use the following code to load the image into a UIImage:

- (UIImage*)loadImage
{
   NSString *cardFileName = [NSString stringWithFormat:@"card%03d", self.card.imageNumber];
   NSString *filePath = [[NSBundle mainBundle] pathForResource:cardFileName ofType:@"jpg"];
   UIImage *cardImage = [UIImage imageWithContentsOfFile:filePath];
   return cardImage;
}
You'll then need to put the UIImage into a UIView that the CardImageViewController is controlling.

Finally, you'll want to go back and add a rightBarButtonItem displaying the UIBarButtonSystemItemAction. That button will be programmatically "wired up" to a target and action in CardListTableViewController similar to what we did with the UIBarButtonItem in StudentManager. Set up the target/action that will push on the settings view controller, ExpansionViewController, whose view has the UISegmentedControl. The tricky thing here is figuring out how to tell the table view that this view is going to go away when the user presses back. (Did someone say delegate?). You'll need to know this so that your table view controller can record the new setting. Once a user makes a change that control, you'll want to update your model in a way that allows you to see only those cards that meet your setting. Don't forget to reload the table when the table reappears or you won't see any changes your model may have made due to the configuration changes.