Categories
Articles

XCode 4 IPhone Mountains of the USA Tutorial: Lesson 7 – Add Detail View


<== Lesson 6 || Overview || Lesson 8 ==>

In Lesson 5 you added touch interaction with the table view and displayed an alert popup. In this lesson you will refactor that to open a second screen in the hierarchy of the NavigationController you set up in Lesson 1.

Detail View Screen

For now you can just use a TextView to display some information about the mountain the user selected. In a later lesson you will redesign that to include a map view showing the mountain’s location.

You have much of the basic structure in place. For instance you need to pass mountain data to the second view. You have the MountainItem objects stored in the NSMutableArray supporting the table. So you can pass the selected table row’s MountainItem object to the detail view.

Source Download

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

[ad name=”Google Adsense”]

Step 1: Create The DetailViewController
Download and uncompress the Starting XCode Project file and open in XCode.

The first task is to add a controller with a view attached.

Select File->New File

New File

Select the UIViewController subclass and select Next.

Choose File Template

Verify that “Targeted for IPad” is unchecked and “With XIB for user interface” is checked. Then select Next.

Choose Options

If you are using the starter file, then this screen should match up. All you need to do is type the file name “DetailViewController” and select Save.

You have options here that are XCode related such as groups for files, the testing target and even the project to contain the file.
Save File

You now have three new files you can see in the XCode Project explorer.

DetailViewContoller Files Created

Step 2: DetailViewController.h – Add TextView and MountainItem Objects
Open the DetailViewController.h file and add the highlighted code.

Lines 2, 7 and 11 include a MountainItem object to hold the data your MainViewController will pass into this class.

Lines 5 and 9 are a UITextView that is used to display some of the data in the MountainItem object.

#import <UIKit/UIKit.h>
#import "MountainItem.h"

@interface DetailViewController : UIViewController {
    UITextView *mountainInfoTextView;
    
    MountainItem *mountainItem;
}
@property (nonatomic, retain) IBOutlet UITextView *mountainInfoTextView;

@property (nonatomic, retain) MountainItem *mountainItem;
@end

[ad name=”Google Adsense”]

Step 3: DetailViewController.m – Include TextView and MountainItem Objects

Open the DetailViewController.m file and add the highlighted code.

This is the routine step to include the header file properties and to handle memory management.

#import "DetailViewController.h"


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

- (void)dealloc
{
    [mountainInfoTextView dealloc];
    [mountainItem dealloc];
    [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.
    
}

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

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

Step 4: DetailViewController.m – Set the DetailViewController UI Data

This step overrides the UIViewController viewWillAppear:animated method.

This allows us to set the NavigationBar title and the UITextview mountainInfoTextView text property with data that you will pass from the MainViewController in a later step.

The viewDidLoad method was not used because it is only called once unless the DetailViewController’s view is removed from the NavigationController navController stack of views. Thus on subsequent mountain choices the view would show the same data from the first time it was added to the NavigationController navController. The NavigationController navController is set up in the application delegate USAMountainsTutorial07AppDelegate.

The viewWillAppear method is called when the view needs to show on the display area.

Line 54 assures the superclass viewWillAppear is executed.

Line 55 sets the title using the MountainItem data received from the MainViewController.

Line 56 places some of the MountainItem data received into the UITextView mountainInfoTextView text property. Nothing fancy is needed here as this is temporary.

-(void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self setTitle:mountainItem.name];
     
    mountainInfoTextView.text = [NSString stringWithFormat: @"Name: %@\nElevation: %f\nLatitude: %f\nLongitude: %f",mountainItem.name, [mountainItem.elevation floatValue],  [mountainItem.latitude floatValue], [mountainItem.longitude floatValue]]; 

}
@end

Step 5: DetailViewController.xib – Set up the TextView and View

Open the DetailViewController.xib in the project explorer.

Select the View and then the Property Inspector. Set the Top Bar property to Navigation Bar.

View Property Inspector

Next in the Objects panel in the lower right drag a TextView object to the View. Size it to fill the screen.

TextView in Objects

This is how the design area should appear:

Text View in Design Layout

With the TextView selected open the Connection Inspector. Drag a “New Referencing Outlet” to the File’s Owner icon. When you release the mouse, select mountainInfoTextView from the popup menu over the File’s Owner Icon. Here is the result:

Text View Connection Inspector

To check your work, keep the Connections Inspector open and select the File’s Owner icon and you should see the following:

File's Owner Connection Inspector

Step 5: MainViewController.h – Add the DetailViewController

Open the MainViewController.h file.

Update line 1 with your server URL.

Add the highlighted lines 4, 21 and 37.

These lines make a DetailViewController object for this class.

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

#import <UIKit/UIKit.h>
#import "DetailViewController.h"

@interface MainViewController : UIViewController <NSXMLParserDelegate, UITableViewDelegate, UITableViewDataSource>
{
    UIButton                *searchButton;
    UIActivityIndicatorView *activityIndicator;
    UITableView             *resultsTableView;
    UILabel                 *elevationLabel;
    UISlider                *elevationSlider;
    
    NSURLConnection         *urlConnection;
    NSMutableData           *receivedData;
    
    NSXMLParser             *xmlParser;
    
    NSMutableArray          *mountainData;
    
    DetailViewController    *detailView;

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

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

@property (nonatomic, retain) NSXMLParser *xmlParser;

@property (nonatomic, retain) NSMutableArray *mountainData;

@property (nonatomic, retain) DetailViewController *detailView;

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

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

[ad name=”Google Adsense”]

Step 6: MainViewController.m – Include the DetailViewController Object

Open the MainViewController.m file and add the highlighted lines.

These lines include the DetailViewController detailView object for this class to manage.

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

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

@synthesize urlConnection;
@synthesize receivedData;

@synthesize xmlParser;

@synthesize mountainData;

@synthesize detailView;


// 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];
    [elevationLabel release];
    [elevationSlider release];
    [urlConnection release];
    [receivedData release];
    [xmlParser release];
    [mountainData release];
    [detailView 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 7"];
    UIBarButtonItem *newBarButtonItem = [[UIBarButtonItem alloc] init];
	newBarButtonItem.title = @"Return";
	self.navigationItem.backBarButtonItem = newBarButtonItem;
	[newBarButtonItem release];
}

- (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;
    self.elevationLabel = nil;
    self.elevationSlider = nil;
    self.detailView = nil;
}

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

The following code does not change and is included here for online reference:

#pragma mark - UI Interface
- (IBAction)sliderChanged:(id)sender
{
    NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
    [numberFormatter setPositiveFormat:@"###,##0"];
    NSString *formattedNumberString = [numberFormatter stringFromNumber:[NSNumber numberWithFloat:elevationSlider.value]];
    elevationLabel.text = [[NSString alloc] initWithFormat:@"Elevation %@ feet",formattedNumberString];
    
    [numberFormatter release];
}
-(IBAction) startSearch:(id)sender
{
    NSLog(@"startSearch");
     // Change UI to loading state
    [self setUIState:LOADING_STATE];
    // Convert the NSSlider elevationValue_ui value to a string
    NSString *elevation = [[NSString alloc ] initWithFormat:@"%.0f", elevationSlider.value]; 
    // 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:
                             @"%@%s%@", kTextURL , "?elevation_min=", elevation];

    //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];
    [elevation 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];
}

#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;
        
    }
    
}
#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 >= 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 7: MainViewController.m – Modify the didSelectRowAtIndexPath to Open the DetailViewController

The lines you need to remove are highlighted here for your convenience:

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

Line 278 is optional to remove if do not want to show the selected mountain when the first screen is shown.

Now add the highlighted lines.

Lines 265 to 272 initialize the DetailViewController. Line 265 checks to see if the DetailViewController detailView object is initialized. If not, the method scoped DetailViewController detailViewTemp object is created from the DetailViewController.xib file on line 269. One line 271 detailViewTemp is set to the class DetailViewController detailView property and then detailViewTemp is released on line 272.

The mountainItemData extracted from the NSMutableArray mountainData is assigned to the detailView mountainItemProperty on line 276.

The last line, 277, pushes the detailView into the NavigationController stack and that causes the detailView to appear.

#pragma mark - Table Delegate Methods
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 
{
     // Row number in the table.
     NSUInteger row = [indexPath row];
     // Data for that row
     MountainItem *mountainItemData = [mountainData objectAtIndex:row];
    // DetailViewController not initialized.
    if (self.detailView == nil)
    {
        // Create a temporary local method variable for DetailViewController
        DetailViewController *detailViewTemp = [[DetailViewController alloc] initWithNibName:@"DetailViewController" bundle:nil];
        // Assign local DetailViewController variable to class instance detailView
        self.detailView = detailViewTemp;
        [detailViewTemp release];
    }
     
    // Pass the selected data to the detailView
    detailView.mountainItem = mountainItemData;
    // Add the detailView to the navigation stack
    [self.navigationController pushViewController:detailView animated:YES];
     
}

The remainder of the MainViewController.m file is included here for online reference.

#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

Try it out in the IPhone simulator.

<== Lesson 6 || Overview || Lesson 8 ==>

Categories
Articles

XCode 4 IPhone Mountains of the USA Tutorial: Lesson 6 – Add Slider to Search By Elevation


<== Lesson 5 || Overview || Lesson 7 ==>

This lesson takes advantage of the server script to select mountains based on their elevation.The script returns mountains that exceed an elevation value you provide as part of the URL request made to the web server.

Slider Search By Elevation

This means you cannot use a static XML file for the UI and code that is added in this section. If you cannot provide a server, you could proceed by skipping this lesson and ignoring the code and UI added. But the code and UI will appear in all future lessons and may serve to confuse you. Best approach is to put the provided PHP script and data file in a folder on a web server and continue with these. These files are included as a part of all the lesson downloads since lesson 2.

The tasks in this lesson are more in adding the UI to provide the user with suitable information to understand what to do with very little screen space. This example choose to use a bit more screen space to help make the selection of an elevation more informative.

Source Download

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

[ad name=”Google Adsense”]
Step 1: MainViewController.h – Add the Slider and Slider Label

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

Open the MainViewController.h in the project navigator window.

Lines 13 and 27 are the UILabel that will appear above the slider. The label shows the value in the slider. You will change the label as the slider is changed.

Lines 14 and 28 are the UISlider.

Line 39 is a IBAction method you link up in the UI to receive messages when the slider is changed.

Remember 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;
    UILabel                 *elevationLabel;
    UISlider                *elevationSlider;

    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) IBOutlet UILabel                  *elevationLabel;
@property (nonatomic, retain) IBOutlet UISlider                 *elevationSlider;

@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;
- (IBAction)sliderChanged:(id)sender;

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

Step 2: MainViewController.m – Add the Slider, Slider Label and Change Navbar Title
Open the MainViewController.m file in the project explorer.

This step is pretty routine.

You need to add in the new UI variables shown on the highlighted lines 11, 12, 40, 41, 77 and 78. These include them in the class and also take care of memory management.

Line 66 is the NavigationBar title you can change.

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

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

@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];
    [elevationLabel release];
    [elevationSlider 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 6"];
}

- (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;
    self.elevationLabel = nil;
    self.elevationSlider = nil;
}

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

Step 3: MainViewController.m – Add the sliderChanged Method.

Add all the code below in the UI Interface section just before the -(IBAction) startSearch:(id)sender method.

When the user changes the slider, you are updating a label that shows the slider value with a comma separated number. For example Elevation 10,000 feet.

[ad name=”Google Adsense”]

You can see at the end of line 91 the NSNumber value property provided by the UISlider: elevationSlider.value. Lines 89 to 91 converting that to a NSString formatted with commas to create NSString *formattedNumberString.

Line 92 assembles NSString *formattedNumberString with the words Elevation and feet and updates the UILabel elevationLabel text property.

You end with the cleanup of the NSNumberFormatter used in the process.

#pragma mark - UI Interface
- (IBAction)sliderChanged:(id)sender
{
    NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
    [numberFormatter setPositiveFormat:@"###,##0"];
    NSString *formattedNumberString = [numberFormatter stringFromNumber:[NSNumber numberWithFloat:elevationSlider.value]];
    elevationLabel.text = [[NSString alloc] initWithFormat:@"Elevation %@ feet",formattedNumberString];
    [numberFormatter release];
}

Step 4: MainViewController.m – Modify the URL to Send the Elevation

You need to add a URL query for example: ?elevation_min=10000. This is needed for the PHP script. You do not need to know how to program PHP but it is included here for reference with a few key lines highlighted to illustrate.

&amp;lt;?php
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT" );
header("Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . "GMT" );
header("Cache-Control: no-cache, must-revalidate" );
header("Pragma: no-cache" );
header("Content-Type: text/xml; charset=utf-8");
// XML to return.
$xml = '';
// Counter for number of mountains returned.
$mountain_count = 0;
// Filter mountains equal to or above this value.
$elevation_min = 12000;
// Check for elevation parameter as a integer.
if ($_REQUEST['elevation_min'] &amp;amp;&amp;amp; intval($_REQUEST['elevation_min']))
{
	$elevation_min = intval( $_REQUEST['elevation_min']);
}
// Each element contains data for one mountain.
$mountains = array();
// Read a CVS file containing mountain data.
$mountain_data_lines = file('mountain_data.csv');
// Each line read .
foreach($mountain_data_lines as $line)
{
	// Strip newline at end of line and break line by comma delimiter and
	// append to $mountains.
	$mountains[] = explode( ',', rtrim($line));
}
// Each mountain.
foreach ($mountains as $value)
{
	// Mountain elevation equals or exceeds the filter value.
	if ( intval($value[1]) &amp;gt;= $elevation_min  )
	{
		$mountain_count++;
		// Create the mountain_item node.
		$xml .= '&amp;lt;mountain_item ';
		$xml .= 'id = "' . $mountain_count . '" ';
		$xml .= 'name = "' . $value[0] . '" ';
		$xml .= 'elevation = "' . $value[1] . '" ';
		$xml .= 'lat = "' . $value[2] . '" ';
		$xml .= 'lon = "' . $value[3] . '" ';
		$xml .= '/&amp;gt;';

	}
}
// Add mountains close node.
$xml .= '&amp;lt;/mountains&amp;gt;';
// Create mountains open node.
$xml_mountains = '&amp;lt;mountains ';
$xml_mountains .= 'source = "http://en.wikipedia.org/wiki/Table_of_the_highest_major_summits_of_the_United_States" ' ;
$xml_mountains .= 'elevation_min = "' . $elevation_min . '" ';
$xml_mountains .= 'count = "' . $mountain_count . '" ';
$xml_mountains .= '&amp;gt;';
// Add mountains open node.
$xml = $xml_mountains . $xml;
// Return xml
echo $xml;
?&amp;gt;

The PHP script provided looks for the elevation_min parameter on lines 14 and 16, absorbs it to the $elevation_min variable and uses $elevation_min on line 33 to filter the returned mountains that have an elevation at or above that value.

A slight modification to the MainViewController.m startSearch method will send the elevation_min parameter to the PHP script.

Line 102 is added to take the slider value and convert to a number.

The single line of code shown on 104 and 105 adds the URL query needed by the PHP script.

-(IBAction) startSearch:(id)sender
{
    //NSLog(@"startSearch");

     // Change UI to loading state
    [self setUIState:LOADING_STATE];
    // Convert the NSSlider elevationValue_ui value to a string
    NSString *elevation = [[NSString alloc ] initWithFormat:@"%.0f", elevationSlider.value];
    // 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:
                             @"%@%s%@", kTextURL , "?elevation_min=", elevation];

    //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];
    [elevation release];
}

The remainder of the code is unchanged and is included here for reference:

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

#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;

    }

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

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

Step 5: MainViewController.xib – Add in the UISlider

Now you can move on to the UI. Refer to the screen shot at the top of the post to keep you on track with the goal of the changes.

Keep in mind, you can do this generally after you have the header definitions in place. You need those so that the Interface Builder part of XCode can display the names of methods and UI components you choose in code. These lessons choose to complete the implementation in the code before moving to UI just to keep the zig zag back and forth that XCode often creates for tutorials. As you get faster, you might want to do the UI right after you do the header files.

Drag a Slider from the Objects library in the bottom right to place it above the Search button.

Slider

Fine tune the position and size to match the tutorial:

Slider Size Inspector

Then you need to wire the MainViewController to the UISlider elevationSlider property defined in step 1 for the MainViewController header and you need to wire the UISlider valueChanged send event to the sliderChanged method you also defined in step 1.

[ad name=”Google Adsense”]

You can use the Properties Inspector to get this done. With the Slider you placed in the design window selected open the Property Inspector. Drag from the “Value Changed” Send Event to the File’s Owner and when you release you can select the sliderChanged in the popped menu above the File’s Owner.

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

Another process you see is to control drag from the Slider in the design window to the File’s Owner in the Related Files panel on left and release mouse. You should see sliderChanged appear in a small menu popped over the File’s Owner. Click and select. Then you can repeat the process in the opposite direction and when you release over the Slider, you should see elevationSlider as a menu choice in the popped menu above the Slider.

The end result is shown here:

Slider Connections Inspector

There are some tweaks needed to make the slider provide the range of elevations and a starting elevation. The range is to match the data available and a starting value to avoid automatic downloads of all the data every time.

So modify the Slider’s Property Inspector as follows:

Slider Property Inspector

Step 6: MainViewController.xib – Add in the UILabel Displaying the Slider Value

Next is a Label above the Slider to show the value of the Slider when it changes.

Drag a Label from the Objects library in the bottom right to place it above the Slider.

Slider

Fine tune the position and size to match the tutorial:

Slider Size Inspector

Set the properties. Note the hard coded match up to display the starting value in the slider.

Slider Property Inspector

Finally you got to wire this label so you can update it in the code. In code you are using the elevationLabel property defined in step 1. Open the Connections Inspector with this Label selected. Drag the New Referencing Outlet to the File’s Owner and after you release the mouse select elevationLabel in the menu popped over the File’s Owner.

Here is the result:

Slider Connections Inspector

Step 7: MainViewController.xib – Add the Minimum and Maximum Labels

This step you add a Label on the left and a Label on the right of the Slider to give the range of elevation values possible. These Labels are static and do not need to be wired to the code.

Drag a Label to the left side of the Slider in the design window:

Left Slider Label

Tweak size and position as follows:

Left Slider Label Property Inspector

The text property for this Label is 12,000 ft. You need to tweak the font size to make the text fit.

The properties as set:

Left Slider Label Size Inspector

Drag a Label to the right side of the Slider in the design window:

Right Slider Label

Tweak size and position as follows:

Right Slider Label Property Inspector

The text property for this Label is 12,000 ft. You need to tweak the font size to make the text fit.

The properties as set:

Right Slider Label Size Inspector

Now try it out in the Simulator.

<== Lesson 5 || Overview || Lesson 7 ==>

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 ==>

Categories
Articles

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

[ad name=”Google Adsense”]
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 &amp;lt;UIKit/UIKit.h&amp;gt;

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

@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;

    }

}

[ad name=”Google Adsense”]

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 &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.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 ==>

Categories
Articles

XCode 4 IPhone Mountains of the USA Tutorial: Lesson 3 – Parse XML Data


<== Lesson 2 || Overview || Lesson 4 ==>

In the last lesson you loaded XML data from the web and displayed in a TextView. In this lesson you are going to parse the XML data and just show the mountain names in TextView.

Screen With Mountains Names Parsed From XML

To do this you will implement the NSXMLParser class and modify your MainViewController to be the NSXMLParser delegate with NSXMLParserDelegate protocol. Protocols are a kind of interface. The NSXMLParser has call backs as it proceeds with the parsing and you need a class to act as the delegate for the NSXMLParser object you create.

You also are going to create your own custom class to represent the data for one mountain. We have limited use for this class in this lesson but it will become handy in passing data around our project in this lesson and as we proceed into the next lessons.

A note about our XML is that the data is only in attributes.

<mountain_item id = "1" name = "Mount McKinley" elevation = "20320" lat = "63.0690" lon = "-151.00063" />

Thus this tutorial does not show you how to parse XML data that would be inside of a node. This makes the XML parsing programming much simpler to do and a great way to get introduced to the overall implementation which also facilitates extracting XML node data when you need to learn how.

There are no UI changes. The one change you will make is in code to display the names of the Mountains in the TextView instead of displaying the XML.

Source Download

  1. Starting XCode 4 Project. This is the lesson 2 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: Create the MountainItem Class

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

This will add our custom class to represent the data for one mountain from the XML file.

Select the USAMountainsTutorial02 folder. Then from the main menu choose File->New->New File

MountainItem – File New

Select Cocoa Touch from the left panel and Objective-C Class from the right panel for the Templates dialog.

MountainItem – Objective C Class

Your class subclasses NSObject.

MountainItem – Options

The file name is MountainItem. The project folder is USAMountainsTutorial02 and the group is also USAMountainsTutorial02. The target USA Mts 02 was done for you in creating the starting project. Targets have to do with deployment and is beyond the scope of this tutorial and not relevant to just using the Simulator.

MountainItem – Save As

You should see two files name MountainItem.h and MountainItem.m in your Project navigator window. If they are not in the USAMountainsTutorial02, just drag them in.

Step 2: MountainItem.h – Add the Instance Variables

Open the MountainItem.h file and add the highlighted lines.

These are the variables that represent the data in the XML file for one Mountain.

//
//
//
#import &amp;lt;Foundation/Foundation.h&amp;gt;
@interface MountainItem : NSObject
{
    NSString *name;
    NSString *elevation;
    NSNumber *latitude;
    NSNumber *longitude;
}
@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSString *elevation;
@property (nonatomic, retain) NSNumber *latitude;
@property (nonatomic, retain) NSNumber *longitude;

@end

Step 3: MountainItem.m – Add the Properties

Open the MountainItem.m file and add line 7.

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

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

@end

Step 4: MainViewController.h – Add the NSXMLParser and Set the NSXMLParserDelegate Protocol

Select the MainViewController.h in the project navigation window on the left and add the highlighted lines.

Line 8 makes your MainViewController a NSXMLParserDelegate for a NSXMLParser and in particular the NSXMLParser object defined on lines 17 and 27. Your xmlParser is now able make calls on NSXMLParserDelegate methods you will add to this class.

If you are working from the Starting XCode Project, update line 4 with your url. This was covered in lesson 2.

//
//
//
#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&amp;gt;
{
    UIButton                *searchButton;
    UIActivityIndicatorView *activityIndicator;
    UITextView              *resultsTextView;

    NSURLConnection         *urlConnection;
    NSMutableData           *receivedData;

    NSXMLParser             *xmlParser;

}
@property (nonatomic, retain) IBOutlet UIButton                 *searchButton;
@property (nonatomic, retain) IBOutlet UIActivityIndicatorView  *activityIndicator;
@property (nonatomic, retain) IBOutlet UITextView               *resultsTextView;

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

@property (nonatomic, retain) NSXMLParser *xmlParser;

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

Step 5: MainViewController.m – Add the NSXMLParser Object and Update Navigation Bar Title

Select the MainViewController.m in the project navigation window on the left and add the highlighted lines.

Two lines, 12 and 35, you can add here are for the NSXMLParser xmlParser object.

Then line 52 update the navigation bar title.

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

@implementation MainViewController
@synthesize searchButton;
@synthesize activityIndicator;
@synthesize resultsTextView;

@synthesize urlConnection;
@synthesize receivedData;

@synthesize xmlParser;

// 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];
    [resultsTextView release];
    [urlConnection release];
    [receivedData release];
    [xmlParser 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.
    [self setTitle:@"USA Mountains Lesson 3"];
}

- (void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.searchButton = nil;
    self.activityIndicator = nil;
    self.resultsTextView = 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];
    }
}

Step 6: MainViewController.m – Start the XML Parsing When Data Loading Completed

Once the NSURLConnection connectionDidFinishLoading method is fired, you can initiate the XML parsing. This is done on lines 162-164.

On line 162 the NSXMLParser is created with the initWithData which conveniently takes the NSMutableData receivedData you got from the NSURLConnection.

[ad name=”Google Adsense”]

Line 163 sets this class as the delegate for the NSXMLParser xmlParser object that you specified in MainViewController.h. To make this work, in MainViewController.h you implemented the NSXMLParserDelegate protocol.

The parsing is kicked of on line 164. In the next step you add one method that NSXMLParser will call.

We are still dumping XML data to the console, so the code for converting the NSMutableData receivedData to a NSString is retained for this lesson but not necessary.

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

Step 7: MainViewController.m – Parse Each mountain_item In XML

At the end of the file you can add the didStartElement method the NSXMLParser calls when it starts a new element in the XML file.

As a reminder here is what your XML node you need to select and parse looks like.

<mountain_item id = "1" name = "Mount McKinley" elevation = "20320" lat = "63.0690" lon = "-151.00063" />

The first step on line 178 is to see if the element being processed matches your XML file’s element mountain_item.

[ad name=”Google Adsense”]

You could have left out lines 180 to 184 for this lesson, but I thought is was a good point to get set up for future lessons where we need to store the mountain item data in an array as a data source to a table.

These lines show how to access an attribute in the method. Your XML file attribute names are name, elevation, lat and lon.

The console will show the mountain names parsed with the NSLog statement on line 186.

On line 188 we append a new line to the UITextView that is just the mountain name.

#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;

    }

}
@end

<== Lesson 2 || Overview || Lesson 4 ==>

Categories
Articles

XCode 4 IPhone Mountains of the USA Tutorial: Lesson 2 – Load XML Data

<== Lesson 1 || Overview || Lesson 3 ==>

In this lesson you will make a requests for the Mountain data from a web site. The data is returned in an XML format. Then for testing you will display the raw XML on the phone screen and also display the XML data in the XCode console.

Screen With XML Loaded From Web

The main goal is to learn to use the NSURL, NSURLRequest, NSURLConnection and NSMutableData classes.

NSURL defines a URL for XCode. NSURLConnection establishes a connection to a server and manages the data to and from that server. NSURLRequest defines a network request along with data to send. NSMutableData stores any returning data.

When you load data from the web or any indeterminate process, you will want to include an activity indicator. So we will add a UIActivityIndicatorView that is the common activity indicator for Mac applications.

To see the data on the phone screen, we will use the UITextView which is a scrollable text component. In future lessons, we will replace the UITextView with a scrolling list of mountains in the XML data we receive.

You will need a web server to complete this and all future lessons in this tutorial. The tutorials use a web server with PHP that reads a comma delimited file containing the mountain data. The XML that the PHP script returns is included in this post should you not have PHP on your server. I will show you how to use that instead of the provided PHP script. However in future lessons we will want to ask the server for just partial data and we need a program to do that. Keep in mind you can also put the mountain data into a database on the server.

Source Download

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

[ad name=”Google Adsense”]

Step 1: MainViewController.h – Define properties and methods.

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

Select the MainViewController.h in the project navigation window on the left and add the highlighted lines.

Line 4 is a constant for the URL to the PHP script or the XML file if you choose not to host the PHP script. More on these choices in this post when we get to those files.

The app will enable and disable the UIButton searchButton, so we need to include it for reference in code.

The UIActivityIndicatorView and UITextView are being added and are also referenced from our UI. The UIActivityIndicatorView will need to be hidden and revealed when we are not and are loading data from the server as well starting and ending its animation. So we need to make it an IBOutlet.

The UITextView will be updated with data coming in from the server and so it also needs to be an IBOutlet.

The MSMutableData is needed to capture the data coming in from the server in a raw format.

Your implementation code will call for disabling and enabling the search button and hiding and unhiding the activity indicator in more than one place in the code. Line 26 defines a method you will use so you do not have to repeat this UI state changing code in more than one place. The method receives an int parameter to define the state of the UI components. You will define their values in the implementation code.

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

#import &lt;uikit uikit.h=""&gt;&lt;/uikit&gt;

@interface MainViewController : UIViewController
{
    UIButton                *searchButton;
    UIActivityIndicatorView *activityIndicator;
    UITextView              *resultsTextView;

    NSURLConnection         *urlConnection;
    NSMutableData           *receivedData;

}
@property (nonatomic, retain) IBOutlet UIButton                 *searchButton;
@property (nonatomic, retain) IBOutlet UIActivityIndicatorView  *activityIndicator;
@property (nonatomic, retain) IBOutlet UITextView               *resultsTextView;

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

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

Step 2: MainViewController.m – Add Properties and Constants

This step basically is the implementation housekeeping prerequisites.

You add the getter and setters for the properties on lines 4 to 9 using synthesize.

Lines 12 and 14 provide constants for states of the view that are used in the setUIState method we defined in the last step. Those places in the code can make the code more readable when calling the setUIState.

#import "MainViewController.h"

@implementation MainViewController
@synthesize searchButton;
@synthesize activityIndicator;
@synthesize resultsTextView;

@synthesize urlConnection;
@synthesize receivedData;

// 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;

Step 3: MainViewController.m – Memory Management Housekeeping

Add the memory release for the searchButton, activityIndicator, activityIndicator, resultsTextView, urlConnection and receivedData in the dealloc method.


- (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];
    [resultsTextView release];
    [urlConnection release];
    [receivedData 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.
}

Step 4: MainViewController.m – Navigation Top Bar Title Updated

On line 47 you might want to update the title so you are not confused when viewing the app.

#pragma mark - View lifecycle

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    [self setTitle:@"USA Mountains Lesson 2"];
}

Step 5: MainViewController.m – More Memory Management

In the viewDidUnload method add these lines to release the subviews you are going to link to this view in the UI.

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

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

Step 6: MainViewController.m – Update the startSearch Method to Fetch Server Data

The startSearch method is already linked to our searchButton from the last tutorial. You have the code below to get the UI in the state for loading in progress, to make a request to the data source on the server and take needed steps based on the success or failure of that connection.

Line 69 is calling a method you will add in the next step to set the UI state. Your constant LOADING_STATE was defined in the last step.

Line 71 creates a NSString for the URL. In a future lesson you are going to concatenate a parameter to send along with the URL and now this line is ready for that.

[ad name=”Google Adsense”]

The NSURLRequest object named req is created on line 74.

The instance object named urlConnection on line 77 is your NSURLConnection. It does all the work for communicating with the server.

You see on line 77 it is using our NSURLRequest req object and also sending the delegate message to make this class, self, its delegate. That means urlConnection can call NSURLConnection methods you add to this class to take action needed to handle notifications such as successful completion or failure.

For this lesson you need to add four methods to handle the NSURLConnection messages didReceiveResponse, didReceiveData, didFailWithError and connectionDidFinishLoading. You will do that just after creating our setUIState method.

Lines 79 to 83 handle a successful connection. A NSMutableData object is created and assigned to the class receivedData NSMutableData object that in later code you will convert to readable XML for display.

Should the connection fail, lines 85 to 97 display a UIAlertView with the error information. Generally you will want to change that to something more meaningful to the user.

#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];

}

Step 7: MainViewController.m – Create the UI State Setting Method setUIState

This is your custom method to set the states of the UI components.

The UIButton has an alpha and enabled property. For your UIButton searchButtonobject, the alpha value is toggled between 50% and 100% and its enabled state is also toggled between true and false.

The UIActivityIndicatorView has methods on lines 108 and 116 for starting and stopping their animation. There is also a property hidesWhenStopped that you will set in the UI design that handles the hiding and showing of our UIActivityIndicator.

-(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 8: MainViewController.m – Clear Received Data When Connection Is Established

You learned about pragma marks in the last lesson. You have 4 NSURLConnection related methods to add and this mark on line 119 is an easy way in XCode to get to where you are placing them in the code.

The connection didReceiveResponse method occurs when a connection is established. When that happens, your data communication starts over. To be on the safe side of it occurring more than once, you clear the NSMutableData object from any previous incomplete attempts. Consider this a boilerplate block you always include in code.

#pragma mark - NSURLConnection Callbacks
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    [receivedData setLength:0];
}

Step 9: MainViewController.m – Accumulate Data Being Received

The NSURLConnection calls the connection didReceiveData method as data arrives and is ready for use. This is called as often as needed depending on the amount of data. The code you need here is to append the data received to your NSMutableData object.

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    [receivedData appendData:data];
}

Step 10: MainViewController.m – Handle Network Connection Failure

The connection didFailWithError NSURLConnection call back method is where you handled the failure of the data transmission. In your case the code displays a UIAlertView with information from the NSError class error object passed in for learning purposes. A better user error should be considered for a released app.

You can use the NSError class to take different action based on the type of error. This is over the scope of this tutorial.

[ad name=”Google Adsense”]

There is some housekeeping such as calling the connection object release method and terminating the NSMutableData receivedData property.

The last line of code calls the sertUIState method with the ACTIVE_STATE value so the UI again appears available for another search.

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

Step 11: MainViewController.m – Handle Network Connection Successful Completion

This final method is called when all the data is successfully loaded. The NSMutableData receivedData object is converted to a NSString on line 147.

On line 151 the UITextView text property resultsTextView is set to the data as a NSString. One the previous line the same is displayed in the XCode console window.

After that the NSURLConnection connection variable is released and the receivedData NSMutable object is truncated.

Your last line has the same task of setting the UI back to a state that the user can search again.

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    // Convert receivedData to NSString.
    NSString *receivedDataAsString = [[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding];

    // Trace receivedData
    NSLog(@"%s - %@", __FUNCTION__, receivedDataAsString);
    resultsTextView.text = receivedDataAsString;
    [receivedDataAsString release];

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

Step 12: MainViewController.xib – Add the Activity Indicator

Open the MainViewController.xib in the Project navigator and drag an Activity Indicator from the Objects library in the bottom right to place it under the button with the Search label.

Activity Indicator View

Be sure you keep the Activity Indicator you placed selected while completing the next three tasks.

Select the size panel in the top right and set the x and y values as shown here.

Activity Indicator Size Inspector

Select the Properties inspector and set the style to Large White and check the Behavior Hides When Stopped.

Activity Indicator Properties

In the Connections Inspector panel drag from the “New Referencing Outlet” in the “Referencing Outlets” group to the File Owner’s icon and release the mouse. Then select activityIndicator. You defined activityIndicator in your MainViewController code as a UIActivityIndicatorView.

Here is how the Connections Inspector will look when you are done.

Activity Indicator Connection Inspector

Step 13: MainViewController.xib – Add the TextView

You are adding a TextView that in a future tutorial you replace with a TableView. So there is no need to get heavily invested in how it looks.

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

Text View

Keep the TextView you placed selected while completing the next two tasks.

Select the size panel in the top right and set the X, Y, Width and Height values as shown here.

Text View Size Inspector

In the Connections Inspector panel drag from the “New Referencing Outlet” in the “Referencing Outlets” group to the File Owner’s icon and release the mouse. Then select resultsTextView defined activityIndicator in your MainViewController code as a UITextView.

Your Connections Inspector should appear as follows.

Text View Connection Inspector

Step 14: MainViewController.xib – Review the Changes

First you can check the layout looking as follows.

MainViewController Design Window Complete

If you select the Connections inspector and then the File Owner’s icon you should see the following. If so you are good to go.

File Owner’s Connection Inspector

Step 15: PHP Server Script or XML File

This IPhone app loads XML data from a server. The Source Download includes an XML file you can use for this lesson.

However, the longer term of the Tutorial will request a query of the data from the server and at that point you can use the PHP script provided. I suggest you use that with this lesson so you are set up. But if you do not have a PHP script enabled server, you can use the XML file for a few more lessons.

Detailing how the PHP script works is beyond the scope of this tutorial. However what you should know it reads a CSV file. Here is a snippet of the file. The name used in the PHP script is mountain_data.csv.

Mount McKinley,20320, 63.0690,-151.00063
Mount Saint Elias,18008,60.2927,-140.9307
Mount Foraker,17400,62.9605,-151.3992

Then the PHP script returns XML. This is a snippet of what the XML data looks like when returned.

&lt;!--?xml version="1.0" encoding="UTF-8"?--&gt;
&lt;mountains source="http://en.wikipedia.org/wiki/Table_of_the_highest_major_summits_of_the_United_States" elevation_min="12000" count="100"&gt;
  &lt;mountain_item id="1" name="Mount McKinley" elevation="20320" lat=" 63.0690" lon="-151.00063"&gt;
  &lt;mountain_item id="2" name="Mount Saint Elias" elevation="18008" lat="60.2927" lon="-140.9307"&gt;
  &lt;mountain_item id="3" name="Mount Foraker" elevation="17400" lat="62.9605" lon="-151.3992"&gt;
&lt;/mountain_item&gt;&lt;/mountain_item&gt;&lt;/mountain_item&gt;&lt;/mountains&gt;

This XML you will learn to parse in a future tutorial.

This is the PHP script. The name I used for the script was get_mountain_data.php. You can name it what you like.

&lt;!--?php
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT" );
header("Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . "GMT" );
header("Cache-Control: no-cache, must-revalidate" );
header("Pragma: no-cache" );
header("Content-Type: text/xml; charset=utf-8");
// XML to return.
$xml = '';
// Counter for number of mountains returned.
$mountain_count = 0;
// Filter mountains equal to or above this value. 
$elevation_min = 12000;
// Check for elevation parameter as a integer.
if ($_REQUEST['elevation_min'] &amp;&amp; intval($_REQUEST['elevation_min']))
{
	$elevation_min = intval( $_REQUEST['elevation_min']);
}
// Each element contains data for one mountain.
$mountains = array();
// Read a CVS file containing mountain data.
$mountain_data_lines = file('mountain_data.csv');
// Each line read .
foreach($mountain_data_lines as $line) 
{
	// Strip newline at end of line and break line by comma delimiter and 
	// append to $mountains.
	$mountains[] = explode( ',', rtrim($line));
}
// Each mountain.
foreach ($mountains as $value)
{
	// Mountain elevation equals or exceeds the filter value.
	if ( intval($value[1]) --&gt;= $elevation_min  )
	{
		$mountain_count++;
		// Create the mountain_item node.
		$xml .= '&lt;mountain_item ';="" $xml="" .="id = &amp;quot;" $mountain_count="" '"="" $value[0]="" $value[1]="" $value[2]="" $value[3]="" ;&lt;="" p=""&gt;
&lt;/mountain_item&gt;

	}
}
// Add mountains close node.
$xml .= '';
// Create mountains open node.
$xml_mountains = '&lt;mountains ';="" $xml_mountains="" .="source = &amp;quot;http://en.wikipedia.org/wiki/Table_of_the_highest_major_summits_of_the_United_States&amp;quot; " ;="" $elevation_min="" '"="" $mountain_count="" ;&lt;br=""&gt;
// Add mountains open node.
$xml = $xml_mountains . $xml;
// Return xml
echo $xml;
?&amp;gt;

<== Lesson 1 || Overview || Lesson 3 ==>

Categories
Articles

XCode 4 IPhone Mountains of the USA Tutorial: Overview

Welcome to the tutorial on creating the Mountains of the USA IPhone using XCode. The links to the completed lessons are located at the end of this post.

The tutorial examples uses the UINavigationController to manage the two screens. I built the first tutorial using a Window-Based Application project and then added the UINavigationController rather than use a Navigation Based project. This helped me understand the wiring of controllers to their interface counterparts and I believe provided a bit more flexibility. I learned to do this with Beginning iPhone 4 Development: Exploring the iOS SDK which was crucial to cracking the mysteries in writing IPhone apps.

The app will parse XML data containing the name, elevation, latitude and longitude of the highest 100 mountains in the USA. The XML data is loaded from a web server PHP script that allows for returning all 100 mountains or a selection above a specific elevation in feet. Then the location of any mountain is displayed in a map view.

Search Screen

Detail Screen

The first screen contains the user input to search for USA mountains based on their elevation and displays the results in a UITableView. Touching a mountain the UITableView opens the second screen where you can see the location of the mountain in a MapView with an MKAnnotationView showing the pin location on the map and a title.

Prerequisites: This is designed for beginners who have experimented with XCode and IPhone projects and who want work through an example that connects concepts into a useful app.

You need to have XCode installed. I used XCode 4 and IOS 4.2 for the tutorial projects. You can use XCode 3 however many screens and menus have changed for the better I might add which will make you dig a bit to find a panel or window. Otherwise the functionality you need is the same.
[ad name=”Google Adsense”]
You need to know how to build a basic hello world XCode IPhone project and run in the Simulator. You should have some basic practice using Interface Builder and adding IBOutlets and IBActions in Objective C.

You need to be a practicing programmer in some language. You do not need to be a ninja. A basic understanding of Objective C is helpful but if you have programming background, you should pick up on the Objective C nuances.

IOS Classes Used: In the tutorial you use the UINavigationController, UITableView, UITableViewCell, UITableViewCellStyleDefault, MKMapView, MKAnnotationView, MKPinAnnotationView, NSNumber, NSNumberFormatter, NSNumberFormatterDecimalStyle, UIActivityIndicatorView, UISlider, NSMutableData, NSMutableArray, UIButton, NSString, NSUInteger, CLLocationCoordinate2D, UIAlertView and NSXMLParser.

[ad name=”Google Adsense”]
Currently there are nine lessons and this will provide a table of contents for them as they get posted. You can register below and you will get notifications when new lessons are posted.

Categories
Articles

XCode IPhone Client Server Echo Hello Example Using PHP


This is a basic IPhone client server example using PHP. This is a duplication of a Titanium example I created February 27th: see Titanium IPhone Client Server Echo Hello Example Using PHP.

This XCode example like the Titanium version simply sends text entered on the phone and returns it with the word “Hello” prefixed. It uses HTTP and the POST protocol. The server app is a simple PHP script.

First Launch

This is the IPhone screen when the application first runs.

The text input will show the keyboard when typing. The send button closes the keyboard and starts the HTTP session. There is no error handling should the connection fail or the server fail.

This is an asynchronous example. So other operations could occur while the request to server is in process. For example the send button can be pressed over and over. So an activity indicator would be a nice improvement.

Source Download
XCode 4 Project

I used the IOS “View-based Application” when creating a new project in XCode. All the coding is in the AnimatedDieFaceViewController UIViewController.

NSUrlClientServerURLVariablesEx01ViewController.h

This sets out the interface for the view controller.

Line 2 contains a constant for the PHP script that received the name and returns the response. You need to replace the YOURDOMAIN and the PATH_IF_ANY_TO/ for your project. The php script is included in the download and is also listed at the end of this post.

The responseTextView is updated with the response from test02.php.

The receivedData NSMutableData object handles the byte data that will come from the server.
[ad name=”Google Adsense”]
The two IBOutlet variables for the controller to access UI components. The controller needs to read the user input from nameTextField and needs to update responseTextView with data received from the server.

The one IBAction method kicks off the client server process with the send button.

#import <UIKit/UIKit.h>
#define kTextUrl @"http://www.YOURDOMAIN.com/PATH_IF_ANY_TO/echo_hello.php"

@interface NSUrlClientServerURLVariablesEx01ViewController : UIViewController {
    UITextField     *nameTextField;
    UITextView      *responseTextView;
    NSMutableData   *receivedData;
}
@property (nonatomic, retain) IBOutlet UITextField *nameTextField;
@property (nonatomic, retain) IBOutlet UITextView *responseTextView;
@property (nonatomic, retain) NSMutableData *receivedData;

- (IBAction)makeRequest;

@end

NSUrlClientServerURLVariablesEx01ViewController.m

This sets out the interface for the view controller. Five methods are used to handle the request to and response from the server.

The makeRequest method initiates the request to the server is on line 10.

First Launch

Four methods on lines 91, 99, 106 and 127 are required because line 27 makes this controller the delegate for NSURLConnection.

The makeRequest method on line 10 is fired from the send button. It performs two tasks. One is assembling the parts of the nsMutableURLRequest object such as the protocol and name value pair parameter. The POST protocol is set on line 18 and line 20 creates the name=whatever_the_user_enters name value pair needed for the server script. The second task is making NSURLConnection and testing if it can work.

Lines 91 – 97 contains the connection didReceiveResponse method. This is needed to clear the data received because of the nature of multiple times it can be called.

The connection didReceiveData method on line 99 – 104 starts collecting the byte data received over the network.

If there is a problem with the connection the connection didFailWithError method occupying lines 106 to 125 is called. Here you can create a better UIAlertView and perhaps relegate the error codes to a trace log.

Finally should everything go well and all the data is received, the connectionDidFinishLoading method on lines 127 – 139 updates the UITextView with our results.

//
#import "NSUrlClientServerURLVariablesEx01ViewController.h"

@implementation NSUrlClientServerURLVariablesEx01ViewController
@synthesize nameTextField;
@synthesize responseTextView;
@synthesize receivedData;

// Send button Touch Up Inside
- (IBAction)makeRequest
{
    NSLog(@"%s", __FUNCTION__);
    //Allocate NSURL object
    NSURL *nsURL = [[NSURL alloc] initWithString:kTextUrl];
    // Allocate NSMutableURLRequest
	NSMutableURLRequest *nsMutableURLRequest = [[NSMutableURLRequest alloc] initWithURL:nsURL];
    // Set HTTP method to POST
	[nsMutableURLRequest setHTTPMethod:@"POST"];
    // Set up the parameters to send.
    NSString *paramDataString = [NSString stringWithFormat:@"%@=%@", @"name", nameTextField.text];
    NSLog(@"%s - paramDataString: %@", __FUNCTION__, paramDataString);
    // Encode the parameters to default for NSMutableURLRequest.
	NSData *paramData = [paramDataString dataUsingEncoding:NSUTF8StringEncoding];
    // Set the NSMutableURLRequest body data.
	[nsMutableURLRequest setHTTPBody: paramData];	
    // Create NSURLConnection and start the request.
    NSURLConnection *nsUrlConnection=[[NSURLConnection alloc]
                                    initWithRequest:nsMutableURLRequest 
                                    delegate:self];
    // Successful connection.
    if (nsUrlConnection) {
        NSMutableData *data = [[NSMutableData alloc] init];
        self.receivedData=data;
        [data release];
    } 
    // Unsuccessful connection.
    else {
        responseTextView.text =  [NSString stringWithFormat :@"Unable to make connection!"] ;
    }  
    // Clean up
	[nsURL release];
	[nsMutableURLRequest release];
    // Close keypad.
    [nameTextField resignFirstResponder];
    

}
- (void)dealloc
{
    [nameTextField release];
    [responseTextView release];
    [receivedData 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

/*
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad
{
    [super viewDidLoad];
}
*/

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

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    // Return YES for supported orientations
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
#pragma mark -
#pragma mark NSURLConnection Callbacks
// Connection response.
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response 
{
    NSLog(@"%s - Response Code: %d\n", __FUNCTION__, [(NSHTTPURLResponse *)response statusCode]);
    NSLog(@"%s - Content-Type: %@\n",  __FUNCTION__, [[(NSHTTPURLResponse *)response allHeaderFields] objectForKey:@"Content-Type"]);
    // Clear the NSMutableData receivedData.
    [receivedData setLength:0];
}
// You got data.
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data 
{
    NSLog(@"%s", __FUNCTION__);
    // Append the data to our NSMutableData receivedData.
    [receivedData appendData:data];
}
// Sorry Dude, connection failed gloriously.
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error 
{
    NSLog(@"%s", __FUNCTION__);
    
    // Create noxious error message.
    NSString *errorMessage = [[NSString alloc]initWithFormat: @"Connection failed! Error - %@ (URL: %@)", [[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]];
    // Throw up a noxious error message.
    UIAlertView *alert = [[UIAlertView alloc] 
                        initWithTitle:@"Sorry Dude!"
                        message:errorMessage 
                        delegate:self
                        cancelButtonTitle:@"Close"
                        otherButtonTitles:nil];
    [alert show];
    // Clean up
    [connection release];
    self.receivedData = nil; 
    [alert release];
    [errorMessage release];
}
// Finally the data is completely loaded.
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    NSLog(@"%s", __FUNCTION__);
    // Encode received data to NSUTF8StringEncoding
    NSString *receviedDataAsString = [[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding];
    NSLog(@"%s - receviedDataAsString: %@", __FUNCTION__, receviedDataAsString);
    // Show received data in the responseTextView.
    responseTextView.text = receviedDataAsString;
    // Clean up.
    [receviedDataAsString release];
    [connection release];
    self.receivedData = nil;
}
@end

[ad name=”Google Adsense”]

NSUrlClientServerURLVariablesEx01ViewController.xib

UIView

This is the layout of the interface.

There is another UIView behind the UILabel, UITextField and UIButton. The UIView background is set to a light gray to give a panel effect.

The UITextField is preloaded with a value.

Below the UIView at the top is a UITextView. It is also preloaded with a message on how to use the app.

 
 
 
 
 
 
 
 
 
 
 
NSUrlClientServerURLVariablesEx01ViewController IBOutlets and IBActions

The IBOutlets are for the UITextField and the UITextView so their properties in the controller code can be read or changed. The UITextField is read on line 20. The UITextView is changed on line 134.

The view outlet was set when I choose the IOS View-based Application to create the XCode project.

The IBActions are for the one UIButton when the Touch Up Inside event is initiated. You can see the method makeRequestlinked to the UIButton.

[ad name=”Google Adsense”]
echo_hello.php – Server Script
Very simple echo script. The name identifier on line 20 of NSUrlClientServerURLVariablesEx01ViewController.m is picked up in the PHP $_REQUEST object as you might expect. The value of $_REQUEST[‘name’] is appended to ‘Hello’ plus a space and returned without any data markup. Nothing very fancy.

<?php
echo "Hello " . $_REQUEST['name'] . "!";
?>