2011/12

 
 

Introduction

This tutorial introduces you to the notion of displaying data within a table view, and obtaining data from an external source.  It involves creating a new type of view controller known as a TableViewController, and attaching this to an empty application (i.e. not starting with a view, but creating one yourself).  It will also involve you gaining some additional experience in using NSDictionary and NSArray classes.  The aim of the lab is to display elements within a table view, and then respond when a table element is selected.  The views will be constructed programmatically in this application, and thus will not involve using Interface Builder or modifying any XIB files.



Todays Tasks:

Walkthrough - Step 1: Creating the app and adding a new Table View Controller by hand.


Begin by creating an Empty Application application called KingsQueens.  Type in the name into the Product Name field, and if you are using XCode 4.2, also add this to the Class Prefix (see opposite).  Ensure that the device Family is set to iPhone, and that you are not using Core Data, Automatic Reference Counting or Unit Testing.


Take a look at the files in the project - you will notice that the app delegate source (src) and header file have been created, but there are no view controller or nib files.  We will create a new view controller, by adding a new class to the project.  Create a new UIViewController subclass, and give it the name MyRootViewController; before clicking Next, ensure that from the drop down menu, your new class is a subclass of UITableViewController, and that you untick the options for “Targeted for iPad” and “With XIB for user interface”.  You will be creating the interface programmatically for this view controller,


In your App Delegate header file, add an instance of class MyRootViewController, as illustrated below.  Notice that in this case we give a forward declaration of the class MyRootViewController (i.e. @class MyRootViewController;), as we are actually going to import it in the implementation (src) file.  Whilst it is not bad to import header files in other header files, it is better to avoid this, and just import them in src files.  Your app delegate header file should look something like this (note we have not created a property for the ivar - as we don’t want this exposed elsewhere for now):


#import <UIKit/UIKit.h>


@class MyRootViewController;


@interface KingsQueensAppDelegate : UIResponder <UIApplicationDelegate> {

    MyRootViewController *myRootViewController;

}


@property (strong, nonatomic) UIWindow *window;


@end


Create an instance of the new MyRootViewController class in the app delegate src file, calling the initWithStyle method as illustrated below, and add this to the subview of the window.  Remember to release this in the dealloc method.


    myRootViewController = [[MyRootViewController alloc] initWithStyle:UITableViewStylePlain];

    [[self window] addSubview:[myRootViewController view]];


Note that we are not using the dot notation, even though it is used elsewhere in the template code.  You will see references to self.window and [self window] in the code - these are actually the same.  However, as the dot syntax is subtly different to that used in Java, try to avoid using it for now.  Your whole didFinishLaunchingWithOptions method should look something like the code below (note that I have added the comment lines around the code you have just added).  It might vary if you are using an earlier version of XCode, but should still work.


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{

    self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];

    // Override point for customization after application launch.

   

    // =================================================================================

    myRootViewController = [[MyRootViewController alloc] initWithStyle:UITableViewStylePlain];

    [[self window] addSubview:[myRootViewController view]];

    // =================================================================================

   

    self.window.backgroundColor = [UIColor whiteColor];

    [self.window makeKeyAndVisible];

    return YES;

}


You can try compiling now, but will see some warnings, as we have still not completed all the code necessary for the new view controller.  When you run the application, you should see a set of empty cells (separated by horizontal lines, which is also scrollable.  This is our table view.


Walkthrough - Step 2: Adding data to the Table View Controller.


In the previous step, we created an empty Table View Controller.  This is a special view controller designed for displaying tables, and simplifies their creation by including the table and data delegate methods that would normally have to be defined.  In particular, there are several methods used to provide the table view class with information about the data it will display.  The following two methods: numberOfSectionsInTableView and tableView:numberOfRowsInSection appear in your table view controller subclass, and need to be modified to inform the table how much data you have.


- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView

{

#warning Potentially incomplete method implementation.

    // Return the number of sections.

    return 0;

}


Tables can separate data into a number of sections, each with their own header and footer.  Once the table knows how many sections there are, it will then call tableView:numberOfRowsInSection: for each section to find out how many table rows there are.


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

{

#warning Incomplete method implementation.

    // Return the number of rows in the section.

    return 0;

}


There is a third method tableView:cellForRowAtIndexPath: which will be called every time a new cell in the table needs to be filled in with the data.  However, before updating all of these methods, more details needs to be know. about the data.  In this case, we will add some explicit data to our application.


Start by uncommenting the initWithStyle method, and create a new NSArray called myDataArray with contents given below.  Note that this array includes a nil element as the very last element - this is used by the view to know when there are no more elements in the array.  Don’t forget to declare myDataArray in the class header file as an ivar, and release it in dealloc. The full implementation of initWithStyle is shown below - the new code has been added between the commented lines.


- (id)initWithStyle:(UITableViewStyle)style

{

    self = [super initWithStyle:style];

    if (self) {

        // Custom initialization

        // =================================================================================

        myDataArray = [[NSArray alloc] initWithObjects:@"Henry VII", @"Henry VIII",

                       @"Edward VI", @"Jane", @"Mary I", @"Elizabeth I", nil];

        // =================================================================================

    }

    return self;

}


The class can now be updated to determine the number of sections and data elements.  Return the value 1 for the number of sections (currently it returns 0).  As the data is stored in an array, we can use the count method for the array to determine the number of elements; i.e. [myDataArray count].  Note: once you have updated these two methods, you should remove the line “#warning Incomplete method implementation” from each, to avoid generating the compiler warnings.


Finally, you need to determine what data should appear in each cell of the table.  This is done in the tableView:cellForRowAtIndexPath: method.  This method is called each time the view constructs a new cell to include in the table.  For the moment, we simply want to define the text that should appear in the cell. At the end of the method (before returning the cell), include the line below.  This retrieves the main label view within the cell, and sets the text for that label view based on one of the elements in the array (indexed by row within indexPath.


        ...

    // Configure the cell...

    [[cell textLabel] setText:[myDataArray objectAtIndex:[indexPath row]]];

   

    return cell;

}


Compile the code and check that the application works.


Walkthrough - Step 3: Sections and Table Styles


So far we have assumed that the table contains only one section.  In this step, we extend the number of sections, and include the section title in the list.  This involves three stages: 1) Creating the data for each section; 2) Modifying all methods that pass in an NSIndexPath to return information regarding the appropriate section, as well as the relevant row; and 3) Displaying the section titles.


1) There are several ways of modelling the data; in this case we will use a combination of arrays and dictionaries.  One array will hold all of our data, corresponding to four of the Houses of English Monarchs: Lancaster, York, Tudor and Stuart.  As each house comprises a title, array of monarchs and period of reign, a dictionary will be used to represent this data.


  1. 1)Start by creating the method initKingsQueensDataModel modifying which defines our data.  Note that we use class factories to create our monarch arrays and dictionaries, as the monarch data will be retained when added to the dictionaries, and at the end of the method, the dictionaries will be added to an array which is explicitly allocated (as we will keep this around).  If you are not clear about the difference between using alloc and using class factories, then please ask one of the demonstrators for clarification!!!


- (void) initKingsQueensDataModel {

    // ==================

    // House of Lancaster

    NSArray *lancasterMonarchs = [NSArray arrayWithObjects:

                                  @"Henry IV", @"Henry V", @"Henry VI", nil];

    NSDictionary *lancaster = [NSDictionary dictionaryWithObjectsAndKeys:

                               @"House Of Lancaster", @"title",

                               lancasterMonarchs, @"monarchs",

                               @"From 1399-1471", @"epoch", nil];

    // ==================

    // House of York

    NSArray *yorkMonarchs = [NSArray arrayWithObjects:

                             @"Edward IV", @"Edward V", @"Richard III", nil];

    NSDictionary *york = [NSDictionary dictionaryWithObjectsAndKeys:

                          @"House Of York", @"title",

                          yorkMonarchs, @"monarchs",

                          @"From 1461-1485", @"epoch", nil];


    // ==================

    // House of Tudor

    NSArray *tudorMonarchs = [NSArray arrayWithObjects:

                              @"Henry VII", @"Henry VIII", @"Edward VI", @"Jane",

                              @"Mary I", @"Elizabeth I", nil];

    NSDictionary *tudor = [NSDictionary dictionaryWithObjectsAndKeys:

                           @"House Of Tudor", @"title",

                           tudorMonarchs, @"monarchs",

                           @"From 1485-1603", @"epoch", nil];


    // ==================

    // House of Stuart

    NSArray *stuartMonarchs = [NSArray arrayWithObjects:

                              @"James I", @"Charles I", @"Charles II", @"James II", @"Mary II",

                              @"William III", @"Anne", nil];

    NSDictionary *stuart = [NSDictionary dictionaryWithObjectsAndKeys:

                            @"House Of Stuart", @"title",

                            stuartMonarchs, @"monarchs",

                            @"From 1603-1707", @"epoch", nil];


   

    myKingQueensModel = [[NSArray alloc] initWithObjects:lancaster, york, tudor, stuart, nil];

   

}


  1. Replace the definition of myDataArray in initWithStyle: with a call to the above method. Remove the ivar myDataArray from the header file and replace it with a new myKingQueensModel ivar.


  1. 2)The next step is to update the data methods to retrieve the correct data.  Now that the array myKingQueensModel now contains the four sections, we can update the method numberOfSectionsInTableView: to return the number of elements in this array. 


- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView

{

    // Return the number of sections.

    return [myKingQueensModel count];

}


  1. However, obtaining the number of rows in each section is a little more convoluted, as we need to first extract the dictionary for each section, find the monarchs array and count the number of elements:


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

{

    // Return the number of rows in the section.

    NSDictionary *house = [myKingQueensModel objectAtIndex:section];

    NSArray *monarchs = [house objectForKey:@"monarchs"];

    return [monarchs count];

}


  1. The method tableView:cellForRowAtIndexPath: utilises the NSIndexPath object to refer to an item with a given row and section; this is done using the NSIndexPath row and section methods respectively.  Therefore, we can use these to index into the myKingQueensModel array to identify the section and the relevant monarchs array to identify the monarch.  As before, to access this array element, we also need to unpack the dictionary:


  1. -(UITableViewCell *)tableView:(UITableView *)tableView

         cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

    ...

       

    // Configure the cell...

    NSDictionary *house = [myKingQueensModel objectAtIndex:[indexPath section]];

    NSArray *monarchs = [house objectForKey:@"monarchs"];

    [[cell textLabel] setText:[monarchs objectAtIndex:[indexPath row]]];


   

    return cell;

}


  1. 3)If you were to compile the code now, you would see all of the cells appear, as a long list (see opposite, right).  Table Views come in two basic styles: “grouped”  (UITableViewStyleGrouped) - where cells within a section appear as rows embedded in a rounded rectangle, and “plain” (UITableViewStylePlain - where the cells appear within cells that stretch from each side of the display. The choice of style is determined when the controller was initialised.  You can change this by modifying the method  application:didFinishLaunchingWithOptions: in the app delegate src file.  Compile, and make sure that you can generate both styles of tables.


  2. The next step is to modify the headers and footers of each section.  At each stage, you may want to compile using either table style to see how they look; however, this lab will assume you are using the UITableViewStylePlain style.


  3. Whenever the beginning or end of a section is displayed, methods are called to determine what should be displayed.  We can make use of the other data - the house title and the epoch (i.e. the dates during which the houses reigned) to fill in the headers and footers.  Add the following two methods to MyRootViewController.m and look at how they appear in both table styles.  Note how we unpacked the data from the dictionaries in each case, by using strings as keys.


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

{

    // Configure the cell...

    NSDictionary *house = [myKingQueensModel objectAtIndex:section];

    return [house objectForKey:@"title"];

}


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

{

    // Configure the cell...

    NSDictionary *house = [myKingQueensModel objectAtIndex:section];

    return [house objectForKey:@"epoch"];

}


Walkthrough - Step 4: Cell Styles and loading JSON data


So far we have hard coded the data within the method initKingsQueensDataModel. However, tables are often generated from data obtained from a local file or from the internet. This step walks you through the process of obtaining data from a JSON file (retrieved locally within the bundle, or from a URL).  We make use of a freeware JSON parser, which you should add to your project.  This code provides methods fro generating or parsing JSON, by creating categories for existing classes (we’ll cover both JSON and categories in more detail towards the end of the course).


In particular, the JSON parser provides an additional method for the NSString class that converts JSON formatted text into either an NSArray or NSDictionary (depending on the data).  We therefore obtain the JSON file as a string, and then “unpack” the data using the parser.  Once it has been unpacked, we simply extract and use the relevant arrays.


Start by downloading the JSON parser as a zip file.  Expand the zip file (if it is not expanded automatically) and drag the JSON directory to your “Other Sources” directory within XCode.  Make sure that the “Copy items into destination group’s folder (if needed)” box is ticked, as you want to include the items in your project.  Then include the following line in the MyRootViewController.m src file:


#import "SBJSON.h"


Modify the initKingsQueensDataModel method within your table-view-controller class so that it loads and unpacks a JSON file containing the same data model as that originally generated programmatically.  You will be using the file Monarchs1.json, which was actually created from the myKingQueensModel generated earlier.


The sample code below loads the data from a URL, but the file could also be stored as part of the bundle (add the “Monarchs1.json” data file to your resources) and loaded locally.  The code for this has been commented out below, but involves constructing the file path from the application bundle, and then loading this file in to construct a string.


- (void) initKingsQueensDataModel {

       

    myKingQueensModel = nil;

    // Generate the URL request for the JSON data

    NSURL *url = [NSURL URLWithString:@"http://www.csc.liv.ac.uk/people/trp/Teaching_Resources/COMP327/Monarchs1.json"];

   

    // Get the contents of the URL as a string

    NSString *jsonString = [NSString stringWithContentsOfURL:url

                                                    encoding:NSUTF8StringEncoding error:nil];

   

    /*

     // Get the contents of the JSON file as a string

     NSString *filePath = [[NSBundle mainBundle]

                           pathForResource:@"Monarchs1" ofType:@"json"]; 

     NSString *jsonString = [NSString stringWithContentsOfFile:filePath

                                                      encoding:NSUTF8StringEncoding error:nil];

     */

    if (jsonString) { 

        myKingQueensModel = [jsonString JSONValue];

        [myKingQueensModel retain];

    }   

}


The JSON parser will construct the data structure as an autoreleased object, as typically it would be accessed and then released.  However, we want to keep this object for use later, and thus we retain it.


As the JSON data represents the same data structure as that used in the earlier steps the code will just “work”.  However, when using JSON in other situations, you should look at the data and understand the structure, so that you can then understand how to modify your code.


Compile the code, and make sure it works as before.  Look at the file:


    http://www.csc.liv.ac.uk/people/trp/Teaching_Resources/COMP327/Monarchs1.json


to examine the structure of the data.  Compare that to the second JSON file, and determine how they differ:


    http://www.csc.liv.ac.uk/people/trp/Teaching_Resources/COMP327/Monarchs2.json


In this file, the array corresponding to the dictionary key “monarchs” has changed from containing elements that are strings, to elements that are themselves dictionaries with two keys - “name” and “reigned”.  If you modify the url in your application and then try to run the application with the new JSON data source, your application will fail when  the method tableView:cellForRowAtIndexPath: is called, as you are now using a different underlying data structure.


Modify this method in MyRootViewController.m file to handle the new structure:


    // Configure the cell...

    NSDictionary *house = [myKingQueensModel objectAtIndex:[indexPath section]];

    NSArray *monarchs = [house objectForKey:@"monarchs"];

    NSDictionary *kingQueen = [monarchs objectAtIndex:[indexPath row]];

    [[cell textLabel] setText:[kingQueen objectForKey:@"name"]];


So far, we have been using the default style for each table cell - UITableViewCellStyleDefault -  whenever a cell is created in tableView:cellForRowAtIndexPath:.  However, there are three other common styles that can be used:


  1. UITableViewCellStyleSubtitle - left-aligns the main title and puts a gray subtitle right under it.

  2. UITableViewCellStyleValue1 - left-aligns the main title and puts the subtitle in blue text and right-aligns it on the right side of the row.

  3. UITableViewCellStyleValue2 - main title in blue and right-aligns it at a point that’s indented from the left side of the row. The subtitle is left-aligned at a short distance to the right of this point.


Note that for UITableViewCellStyleDefault and UITableViewCellStyleSubtitle, images can also be added.


Modify your code to test each style.  You are already modifying the main title (using the UILabel textLabel); you can also modify the subtitle (using the UILabel detailTextLabel), by adding the following line:


    [[cell detailTextLabel] setText:[kingQueen objectForKey:@"reigned"]];



Walkthrough - Step 5: Handling Cell Selection


In this step, we want to determine some action based on the user selecting a row.  When a row is selected, we want to retrieve the associated string from the array (given the indexPath of the selected element).  We then display this within an alertView view, before cleaning up and deselecting the row.


Start by looking at the definition of the method didSelectRowAtIndexPath and in particular, the sample code (which is commented out), which provides the stub for creating a new view controller (based on the table cell selection) and pushing this on to a navigationController stack.


NOTE: this assumes that your tableViewController is just one of several view controllers in the application, and that the root view controller is a navigation view controller.


For now, however, we ignore this code, as we simply want to display a message.  Construct the message, by retrieving the monarch referenced by the indexPath and identifying their name (see previous step), and including it into a message string:


    NSDictionary *house = [myKingQueensModel objectAtIndex:[indexPath section]];

    NSArray *monarchs = [house objectForKey:@"monarchs"];

    NSDictionary *kingQueen = [monarchs objectAtIndex:[indexPath row]];   

    NSString *title = [NSString stringWithFormat:@"You selected %@.",

                       [kingQueen objectForKey:@"name"]];


Then create an instance of the UIAlertView class, and set the title and message content as shown below.  This class is useful for creating popups, which can also include additional buttons to request confirmation from a user.  For now, we simply want a dismiss button.  Once the popup appears (by calling the show method on it), we should clean up the memory (by releasing the instance; the instance will be retained by the application once the show method is called until the popup is closed, therefore we do not need to keep a copy ourselves) and then deselecting the row.  Note that we set animated to YES so that the selection fades nicely!  Ass the following code after constructing the string title, and compile the code, checking to see that it works.


    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:title

                message:@"Now what?" delegate:nil

                cancelButtonTitle:@"Dismiss" otherButtonTitles:nil];

    [alertView show];

    [alertView release];

   

    [tableView deselectRowAtIndexPath:indexPath animated:YES];


Additional Task 1

Try modifying the code to also display an image next to each of the table entries.  Start by identifying a single image file, adding it to your project, and using the following code to add it to the cell:


    UIImage *crownImage = [UIImage imageNamed:@"crown.png"];

    [[cell imageView] setImage:crownImage];


This assumes that you have maned the image file crown.png


Additional Task 2

Download the JSON file and add it to your project.  Modify the method initKingsQueensDataModel to load this file from the local bundle (instead from from the URL), and ensure that this works.  Then try modifying the JSON file to add additional key-value pairs to each monarch, or different data to the houses.  You can independently check whether your JSON file is valid by visiting the JSON Lint validator.


Additional Task 3

Try modifying the JSON file to include the URL of an image for each house, or for each monarch.  Modify your code to load this image from the URL, and add it to the cells.  Note how the loading of each image can affect the responsiveness of your application - this is something we will look at in future labs.