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