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