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