<== 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.
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
- Starting XCode 4 Project. This is the lesson 6 project completed.
- PHP and CSV Files. Script to read data file and selects by elevation and returns XML. See Lesson 2.
- 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
Select the UIViewController subclass and select Next.
Verify that “Targeted for IPad” is unchecked and “With XIB for user interface” is checked. Then select Next.
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 now have three new files you can see in the XCode Project explorer.
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 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.
Next in the Objects panel in the lower right drag a TextView object to the View. Size it to fill the screen.
This is how the design area should appear:
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:
To check your work, keep the Connections Inspector open and select the File’s Owner icon and you should see the following:
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 ==>