XCode 4 IPhone Mountains of the USA Tutorial: Lesson 4 – Add UITableView

<== Lesson 3 || Overview || Lesson 5 ==>

In the last lesson you parsed the USA Mountain XML data from the web and displayed the mountain names in TextView. In this lesson you replace the TextView with a TableView. In the TableView you will display the mountain names along with their elevations.

Screen With TableView

You use a UITableView and it uses a data source for the items it displays. The data source that you use in this lesson is a NSMutableArray. Items in the NSMutableArray are used to create UITableViewCell objects that are added to the UITableView.

In the NSXMLParser you can take each mountiain in the XML data, place it’s data into a MountainItem object you created in the last lesson and store the MountainItem object in the NSMutableArray.

A UITableView needs a UITableViewDataSource delegate object to call methods. You can make the MainViewController that object and then tie them together in the MainViewController interface.

Source Download

  1. Starting XCode 4 Project. This is the lesson 3 project completed.
  2. PHP and CSV Files. Script to read data file and selects by elevation and returns XML. See Lesson 2.
  3. Mountain XML Data. Alternative to hosting PHP script – See Lesson 2.
  4. Completed XCode 4 Project


Step 1: MainViewController.h – Add UITableView, NSMutableData and UITableViewDataSource

Download and uncompress the Starting XCode Project file and open in XCode.

Open the MainViewController.h in the project navigator window.

You can make this class receive messages for a UITableViewDataSource. To do that you add the UITableViewDataSource protocol to line 8.

Line 12 and 23 replaces the UITextView with UITableView and note that you have a different object name you are using.

The data from the XML you can place in a NSMutableArray. So lines 18 and 29 get that set up.

If you are working from the Starting XCode Project, update line 4 with your url.

//
//
//
#define kTextURL    @"http://YOUR_DOMAIN/PATH_IF_ANY_TO_SCRIPT/PHP_SCRIPT_OR_XML_FILE"

#import <UIKit/UIKit.h>


@interface MainViewController : UIViewController <NSXMLParserDelegate, UITableViewDataSource>
{
    UIButton                *searchButton;
    UIActivityIndicatorView *activityIndicator;
    UITableView             *resultsTableView;
    
    NSURLConnection         *urlConnection;
    NSMutableData           *receivedData;
    
    NSXMLParser             *xmlParser;
    
    NSMutableArray          *mountainData;

}
@property (nonatomic, retain) IBOutlet UIButton                 *searchButton;
@property (nonatomic, retain) IBOutlet UIActivityIndicatorView  *activityIndicator;
@property (nonatomic, retain) IBOutlet UITableView              *resultTableView;

@property (nonatomic, retain) NSURLConnection *urlConnection;
@property (nonatomic, retain) NSMutableData *receivedData;

@property (nonatomic, retain) NSXMLParser *xmlParser;

@property (nonatomic, retain) NSMutableArray *mountainData;

-(IBAction) startSearch:(id)sender;
- (void) setUIState:(int)uiState;

@end


Step 2: MainViewController.m – Add UITableView, NSMutableData Objects and Update Navigation Bar Title

This step gets the new variables included and sets the title.

Open the MainViewController.m in the project navigator window.

The highlighted lines 10, 37 and 72 you are replacing the resultsTextView object with the resultTableView object.

Lines 17 and 41 add in the NSMutableData mountainData object.

You need to initialize the NSMutableData mountainData object. Line 59 allocates memory and initializes and the next line keeps it out of the way of the garbage collector.

Update the navigation bar title on line 62.

//
//
//
#import "MainViewController.h"
#import "MountainItem.h"

@implementation MainViewController
@synthesize searchButton;
@synthesize activityIndicator;
@synthesize resultTableView;

@synthesize urlConnection;
@synthesize receivedData;

@synthesize xmlParser;

@synthesize mountainData;


// State is loading data. Used to set view.
static const int LOADING_STATE = 1;
// State is active. Used to set view.
static const int ACTIVE_STATE = 0;



- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)dealloc
{
    [searchButton release];
    [activityIndicator release];
    [resultTableView release];
    [urlConnection release];
    [receivedData release];
    [xmlParser release];
    [mountainData release];
    [super dealloc];
}

- (void)didReceiveMemoryWarning
{
    // Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];
    
    // Release any cached data, images, etc that aren't in use.
}

#pragma mark - View lifecycle

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    mountainData = [[NSMutableArray alloc] init];
    [mountainData retain];

    [self setTitle:@"USA Mountains Lesson 4"];
}

- (void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.searchButton = nil;
    self.activityIndicator = nil;
    self.resultTableView = nil;
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    // Return YES for supported orientations
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

Step 3: MainViewController.m – No Changes for #pragma mark – UI Interface

The code in the UI Interface pragma mark remains unchanged and is added here for completeness.

#pragma mark - UI Interface
-(IBAction) startSearch:(id)sender
{
    NSLog(@"startSearch");
    
     // Change UI to loading state
    [self setUIState:LOADING_STATE];
    // Create the URL which would be http://YOUR_DOMAIN_NAME/PATH_IF_ANY_TO/get_usa_mountain_data.php?elevation=12000
    NSString *urlAsString = [NSString stringWithFormat:@"%@", kTextURL ];

    NSLog(@"urlAsString: %@",urlAsString );
    NSURLRequest *req = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:urlAsString]];
    // Create the NSURLConnection con object with the NSURLRequest req object 
    // and make this MountainsEx01ViewController the delegate.
    NSURLConnection *con =[[NSURLConnection alloc] initWithRequest:req delegate:self];
    // Connection successful
    if (con) {
        NSMutableData *data = [[NSMutableData alloc] init];
        self.receivedData=data;
        [data release];
    } 
    // Bad news, connection failed.
    else 
    {
        UIAlertView *alert = [
                              [UIAlertView alloc] 
                              initWithTitle:NSLocalizedString(@"Error", @"Error")
                              message:NSLocalizedString(@"Error connecting to remote server", @"Error connecting to remote server")
                              delegate:self 
                              cancelButtonTitle:NSLocalizedString(@"Bummer", @"Bummer")
                              otherButtonTitles:nil
                              ];
        [alert show];
        [alert release];
    }
    [req release];
    
}
-(void) setUIState:(int)uiState;
{
    // Set view state to animating.
    if (uiState == LOADING_STATE)
    {
        searchButton.enabled = false;
        searchButton.alpha = 0.5f;
        [activityIndicator startAnimating];
        
    }
    // Set view state to not animating.
    else if (uiState == ACTIVE_STATE)
    {
        searchButton.enabled = true;
        searchButton.alpha = 1.0f;
        [activityIndicator stopAnimating];
    }
}

Step 4: MainViewController.m – NSURLConnection Reset Table For New Search Data

In the connectionDidFinishLoading remove this code you used to display data in the TextView from the last lesson. You could keep lines 164 and 167 should you want to trace the code coming in, but by now that functionality should work.

Open this source and remove the highlighted lines.

#pragma mark - NSURLConnection Callbacks
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response 
{
    [receivedData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data 
{
    [receivedData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error 
{
    [connection release];
    self.receivedData = nil; 
    
    UIAlertView *alert = [[UIAlertView alloc] 
                          initWithTitle:@"Error"
                          message:[NSString stringWithFormat:@"Connection failed! Error - %@ (URL: %@)", [error localizedDescription],[[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]] 
                          delegate:self
                          cancelButtonTitle:@"Bummer"
                          otherButtonTitles:nil];
    [alert show];
    [alert release];
    // Change UI to active state
    [self setUIState:ACTIVE_STATE];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection 
{
    // Convert receivedData to NSString.
    NSString *receivedDataAsString = [[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding];
    
    // Trace receivedData
    NSLog(@"connectionDidFinishLoading %@", receivedDataAsString);
    resultsTextView.text = @"";
    [receivedDataAsString release];
    
    xmlParser = [[NSXMLParser alloc] initWithData:receivedData];
    [xmlParser setDelegate:self]; 
    [xmlParser parse];

    // Connection resources release.
    [connection release];
    self.receivedData = nil;
    // Change UI to active state
    [self setUIState:ACTIVE_STATE];
}

Now you can the needed code highlighted in the next code block.

There are two additions for your NSURLConnection connectionDidFinishLoading method.

First you need to clear the NSMutableData mountainData object before parsing. Line 164 in the connectionDidFinishLoading is an ideal place as you know there is new data successfully loaded.

Second the UITableView resultTableView object needs a refresh once the data is parsed and loaded into your NSMutableArray mountainData object. This occurs with changes to the XML parsing methods you do in upcoming steps.

On line 170 after the XMLParser has completed, reloadData message is sent to the UITableView resultTableView object and it will reset the data displayed from the NSMutableArray mountainData object.

#pragma mark - NSURLConnection Callbacks
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response 
{
    [receivedData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data 
{
    [receivedData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error 
{
    [connection release];
    self.receivedData = nil; 
    
    UIAlertView *alert = [[UIAlertView alloc] 
                          initWithTitle:@"Error"
                          message:[NSString stringWithFormat:@"Connection failed! Error - %@ (URL: %@)", [error localizedDescription],[[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]] 
                          delegate:self
                          cancelButtonTitle:@"Bummer"
                          otherButtonTitles:nil];
    [alert show];
    [alert release];
    // Change UI to active state
    [self setUIState:ACTIVE_STATE];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection 
{
    // Clear the NSMutableData the tUITableView uses.
    [mountainData removeAllObjects];
    // Do the Parsing
    xmlParser = [[NSXMLParser alloc] initWithData:receivedData];
    [xmlParser setDelegate:self]; 
    [xmlParser parse];
    // Tell the UITableView to reload the data
    [self.resultTableView reloadData];

    // Connection resources release.
    [connection release];
    self.receivedData = nil;
    // Change UI to active state
    [self setUIState:ACTIVE_STATE];
}

Step 5: MainViewController.m – Place the XML Data in the TableView Data Source

First you have some clean up. So you need to remove the code highlighted. Line 192 you could just comment in case you need to debug. Line 194 was for the UITextView you are removing and replacing with a UITableView.

Open this source and remove the highlighted lines.

#pragma mark - NSXMLParser Callbacks
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI 
 qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
    // NSLog(@"%s", __FUNCTION__);
    //Is a mountain_item node
    if ([elementName isEqualToString:@"mountain_item"])
    {
        MountainItem *mountainItem = [[MountainItem alloc] init];
        mountainItem.name = [attributeDict objectForKey:@"name"];
        mountainItem.elevation = [attributeDict objectForKey:@"elevation"];
        mountainItem.latitude = [attributeDict objectForKey:@"lat"];
        mountainItem.longitude = [attributeDict objectForKey:@"lon"];
        
        NSLog(@"mountainItem.name: %@", mountainItem.name);
        
        resultsTextView.text = [NSString stringWithFormat:@"%@%@\n", resultsTextView.text, mountainItem.name];
        
        [mountainItem release];
        mountainItem = nil;
        
    }
    
}

Now you can add the mountainItem object to the NSMutableArray mountainData object. Each item in mountainData is used to feed the UITableView you will see in code coming up.

#pragma mark - NSXMLParser Callbacks
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI 
 qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
    // NSLog(@"%s", __FUNCTION__);
    //Is a mountain_item node
    if ([elementName isEqualToString:@"mountain_item"])
    {
        MountainItem *mountainItem = [[MountainItem alloc] init];
        mountainItem.name = [attributeDict objectForKey:@"name"];
        mountainItem.elevation = [attributeDict objectForKey:@"elevation"];
        mountainItem.latitude = [attributeDict objectForKey:@"lat"];
        mountainItem.longitude = [attributeDict objectForKey:@"lon"];
        
        //NSLog(@"mountainItem.name: %@", mountainItem.name);
        
        [mountainData addObject:mountainItem];

        [mountainItem release];
        mountainItem = nil;
        
    }
    
}

Step 6: MainViewController.m – Add UITableView Methods To Add The Data

Now you just have two methods to add for the UITableView to call. These are defined by the NSXMLParserDelegate we specified in the MainViewController.h header. In a later step the UITableView resultsTableView is linked to make this class the delegate to receive these messages.

This first method, numberOfRowsInSection, is needed to tell the UITableView resultsTableView object how many rows it is managing. You simply return the total items in the NSMutableArray mountainData object.

#pragma mark - Table View Data Source Methods
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 
{
    //NSLog(@"%s", __FUNCTION__);
    NSLog(@"self.mountainData count: %d", [self.mountainData count]);
    return [self.mountainData count];
}

The cellForRowAtIndexPath is where you manufacture a UITableViewCell and return it to the UITableView resultsTableView.

Line 212 attempts to get a cell available that the UITableView resultsTableView is not using. Lines 215 to 220 handle the case where there are no available cell to use.

The indexPath method argument provides the row you need to fetch data from the NSMutableArray mountainData. Line 222 assures mountainData has that row.

Then on line 225, you get a local copy mountainItem of the MountainItem data stored in the NSMutableArray mountainData for the row being processed. The name and the elevation are put together on line 227 for a label to the row.

On lines 229 and 230 you update the cell and finally at the end of the method the cell is returned for the UITableView to manage.


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
{
    static NSString *SimpleTableIdentifier = @"SimpleTableIdentifier";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:
							 SimpleTableIdentifier];
    // UITableViewCell cell needs creating for this UITableView row.
    if (cell == nil) 
    {
        cell = [[[UITableViewCell alloc]
				 initWithStyle:UITableViewCellStyleDefault
				 reuseIdentifier:SimpleTableIdentifier] autorelease];
    }
    NSUInteger row = [indexPath row];
    if ([mountainData count] - 1 >= row)
    {
        // Create a MountainItem object from the NSMutableArray mountainData
        MountainItem *mountainItemData = [mountainData objectAtIndex:row];
        // Compose a NSString to show UITableViewCell cell as Mountain Name - nn,nnnn 
        NSString *rowText = [[NSString alloc ] initWithFormat:@"%@ - %@ feet",mountainItemData.name, mountainItemData.elevation];
        // Set UITableViewCell cell
        cell.textLabel.text = rowText;
        cell.textLabel.font = [UIFont boldSystemFontOfSize:14];
        // Release alloc vars
        [rowText release];
    }
    return cell;
}

@end

Step 7: MainViewController.xib – Remove the UITextView

Delete the TextView show here.

Delete UITextView

Step 8: MainViewController.xib – Add The UITableView

Now drag a TableView from the Objects library in the bottom right to place it under the button with the Activity Indicator.

Table View

XCode will put in place makers for the header, footer and rows so as you adjust the properties and get a visual sense of your work.

Positioning to fill the bottom of the screen under the UIActivityIndicator can be done in the design window or you can use the values here to match exactly. Adjust the size of the rows as you please. You do not use the header and footer in this tutorial.

Table View Size Inspector

Here is the result:

Delete UITextView

Then you need to wire this TableView to the MainViewController. In the Connections Inspector set the dataSource and deletegate to the File’s Owner by dragging to the File’s Owner icon in the files panel on left.

Repeat for the “New Referencing Outlet” and when you release the mouse over the File’s Owner icon select resultTableView.

Table View Connections Inspector

<== Lesson 3 || Overview || Lesson 5 ==>



Register For Updates
You can opt out at anytime
* indicates required