Categories
Articles

XCode 4 IPhone Mountains of the USA Tutorial: Lesson 5 – Table Touch Interaction


<== Lesson 4 || Overview || Lesson 6 ==>
In the last lesson we added the TableView. In this lesson you handle the event when the user selects an item in the table and popup an UIAlert view. Also in this lesson you add a custom method that comma separates numbers.

Table Touch Alert

You need a UITableViewDelegate class to handle user interaction with the table. You can use the MainViewController class as a UITableViewDelegate by adding the protocol to the MainViewController.h header.

MainViewController will receive messages when the user interacts with the TableView. The particular method you will implement is didSelectRowAtIndexPath. It will receive messages when a user selects a row, called a cell, in the TableView.

In this app you have the mountain elevations that go into the thousands of feet. In the data source the elevation might be 20320 feet for Mount McKinley for example. To display the number it is easier to read with a comma separation as 20,320.

There are a number of steps needed to accomplish this with the data source in this app. To avoid having the steps in the code whereever you need to format a number with comma separators, you can write a method to handle the work. The method will use NSNumber, NSNumberFormatter and NSNumberFormatterDecimalStyle to complete the work.

There are a number of approaches to where to deploy this new method. For example you could use it just before anytime an elevation is displayed. To simplify the code, you are going to create the comma separated elevation one time and store it as a separate item in the MountainItem objects in the NSMutableArray used to populate the table view.

Source Download

  1. Starting XCode 4 Project. This is the lesson 4 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

[ad name=”Google Adsense”]
Step 1: MountainItem.h – Add the elevationAsString Property

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

Open the MountainItem.h in the project navigator window.

The NSString version of the elevation variable is being changed to NSNumber. Actually it could be eliminated since the tutorial lessons planned at this point will not perform math or sorting on the elevation.

Memory is a premium in an IPhone, so if there is no need for a variable, remove it. Storing for each mountain uses more memory, but not enough to be of concern for this app. More of a concern is the amount of data loaded from the server and using lazy loading is a technique to handle that issue but unfortunately outside the scope of the tutorial.

We are adding a variable to hold the comma separated version of the elevation on line 7. One line 6 you are changing the elevation variable from NSString to NSNumber.

Lines 13 and 14 add the properties for these two class members.

#import &amp;lt;Foundation/Foundation.h&amp;gt;

@interface MountainItem : NSObject
{
    NSString *name;
    NSNumber *elevation;
    NSString *elevationAsString;
    NSNumber *latitude;
    NSNumber *longitude;

}
@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSNumber *elevation;
@property (nonatomic, retain) NSString *elevationAsString;
@property (nonatomic, retain) NSNumber *latitude;
@property (nonatomic, retain) NSNumber *longitude;

@end

Step 2: MountainItem.m – Add the elevationAsString Property

Open the MountainItem.h in the project navigator window and update for the elevation and elevationAsString properties.

#import "MountainItem.h"

@implementation MountainItem
@synthesize name, elevation, elevationAsString, latitude, longitude;

@end

Step 3: MainViewController.h – Add the getCommaSeparatedFromStringContainingNumber Method

Open the MainViewController.h in the project navigator window.

You want to make this class receive messages when the user interacts with the table. Line 8 you need to include the UITableViewDelegate protocol and then you can implement methods and receive messages for that protocol in the MainViewController.m implementation.

Add line 36 highlighted below. The method will expect a NSString containing an unformatted number as input and return a NSString with a comma separated number.

Finally be sure to change line 4 to include your url.

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

#import &amp;lt;UIKit/UIKit.h&amp;gt;

@interface MainViewController : UIViewController &amp;lt;NSXMLParserDelegate, UITableViewDelegate, UITableViewDataSource&amp;gt;
{
    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;

-(NSString *) getCommaSeparatedFromStringContainingNumber:(NSString *)stringWithNumber;
@end

[ad name=”Google Adsense”]
Step 4: MainViewController.m – Update Navigation Bar Title

This step sets the title.

Open the MainViewController.m in the project navigator window.

The beginning part of the code is unchanged, and included here for reference.

#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.
}

Here you can change the navigation bar title shown on line 58.

#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 5"];
}

There are no further changes until you parse the data.

Remaining code up to parsing data is unchanged and included here for reference.

- (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);
}

#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.
    urlConnection = [[NSURLConnection alloc] initWithRequest:req delegate:self];
    // Connection successful
    if (urlConnection) {
        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];
    }
}

#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
{
    [mountainData removeAllObjects];
    // Convert receivedData to NSString.

    xmlParser = [[NSXMLParser alloc] initWithData:receivedData];
    [xmlParser setDelegate:self];
    [xmlParser parse];

    [self.resultTableView reloadData];

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

Step 5: MainViewController.m – Create the elevationAsString Value when Parsing XML

The original line 183 remains unchanged.

Add line 184 to create the elevationAsString value. The getCommaSeparatedFromStringContainingNumber method will return the NSString with a comma separated elevation.

#pragma mark - NSXMLParser Callbacks
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI
 qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
    //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.elevationAsString = [self getCommaSeparatedFromStringContainingNumber:[attributeDict objectForKey:@"elevation"]];
        mountainItem.latitude = [attributeDict objectForKey:@"lat"];
        mountainItem.longitude = [attributeDict objectForKey:@"lon"];

        [mountainData addObject:mountainItem];

        [mountainItem release];
        mountainItem = nil;

    }

}

There are no changes to the remaining code. It is added here for reference.

#pragma mark - Table View Data Source Methods
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [self.mountainData count];
}

- (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 &amp;gt;= 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.elevationAsString];
        // Set UITableViewCell cell
        cell.textLabel.text = rowText;
        cell.textLabel.font = [UIFont boldSystemFontOfSize:14];
        // Release alloc vars
        [rowText release];
    }
    return cell;
}

Step 6: MainViewController.m – Add the didSelectRowAtIndexPath

This method is called when the user selects a row on the table. Remember you have a MountainItem object stored in each item in the MSMutableArray mountainData. The method passes an NSIndexPath indexPath variable that contains the row number in the table the user selected. Line 234 ans 235 extract the MountainItem record.

Lines 237 to 248 create a popup UIAlertView that shows the longitude, latitude and the mountain name. In a future lesson, you will open a new detail view for the mountain selected.

Line 249 is optional. The default behavior when the user selects a row in the table is the row show a selected view. So its really a design choice if you want to remove the highlight once the row is selected. Try with line 249 commented and not commented so you can see both.

#pragma mark - Table Delegate Methods
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    //NSLog(@"%s", __FUNCTION__);

     NSUInteger row = [indexPath row];
     MountainItem *mountainItemData = [mountainData objectAtIndex:row];

     NSString *message = [[NSString alloc] initWithFormat:
                          @"Coordinates\nLatitude: %f\nLongitude: %f", [mountainItemData.latitude floatValue], [mountainItemData.longitude floatValue]];
     UIAlertView *alert = [[UIAlertView alloc]
         initWithTitle:mountainItemData.name
         message:message
         delegate:nil
         cancelButtonTitle:@"Close"
         otherButtonTitles:nil];
     [alert show];

     [message release];
     [alert release];
     [tableView deselectRowAtIndexPath:indexPath animated:YES];

}

Step 7: MainViewController.m – Add the getCommaSeparatedFromStringContainingNumber Method

This is the getCommaSeparatedFromStringContainingNumber method implementation.

The first part of the method, 255 to 260, converts the incoming stringWithNumber to a NSNumber.

Lines 256 and 257 create a NSNumberFormatter elevationToNumber variable and sets the style to NSNumberFormatterDecimalStyle.

[ad name=”Google Adsense”]

Then line 259 makes the conversion from NSString to NSNumber myNumber using the NSNumberFormatter numberFromString message. The NSNumber myNumber is used in the second section of the method.

The second part of the method, lines 263 to 267, convert the NSNumber myNumber variable, created in the first part, to a NSString formatted as shown on line 264. It starts out with a second NSNumberFormatter on line 264 that uses the NSNumberFormatter stringFromNumber message to convert the myNumber variable to the return variable formattedNumberString on line 265.

#pragma mark - Utilities
-(NSString *) getCommaSeparatedFromStringContainingNumber:(NSString *)stringWithNumber
{
    // Convert the MountainItem.elevation as a NSString to a NSNumber
    NSNumberFormatter * elevationToNumber = [[NSNumberFormatter alloc] init];
    [elevationToNumber setNumberStyle:NSNumberFormatterDecimalStyle];
    NSString *elevation = stringWithNumber;
    NSNumber *myNumber = [elevationToNumber numberFromString:elevation];
    [elevationToNumber release];

    // Format elevation as a NSNumber to a comma separated NSString
    NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
    [numberFormatter setPositiveFormat:@"###,##0"];
    NSString *formattedNumberString = [numberFormatter stringFromNumber:myNumber];
    [numberFormatter release];
    return formattedNumberString;
}

@end

<== Lesson 4 || Overview || Lesson 6 ==>