Categories
Articles

HTML5 Canvas Bobbing Circle Animation Demonstrating Sine Wave Movement

This is an example of using the HTML5 canvas and using JavaScript Math.sin to produce a bobbing animation of a circle.

Demo

Download Source

This example is in one HTML document that contains all the JavaScript.

Foundation HTML5 Animation with JavaScript
Learn More

JQuery is included only to detect when the document is ready and start the timer for the animation. The timer calls an update function which updates the data values for the items on the canvas. In this case it is just the animated object. The timer also calls the draw function that calls the animated object’s draw function.

I am using the Google CDN for JQuery.

This example adds the canvas node to the body dynamically in JavaScript and is just a technique. You could include the canvas tag statically.

You can view the full html file at the end of the post. I will excerpt parts to explain what is going on inside the code.

[ad name=”Google Adsense”]

Once JQuery is ready the animation timer is started. The timer calls the update and draw methods.

	// JQuery ready
	$(function() 
	{
		// Start here.
		// Timer for animation.
		setInterval(function() 
		{
			update();
			draw();
		}, 1000/FPS);
	});

This is the draw method. It simply clears the canvas, draws the canvas background and border, and then calls the draw method of the animation object called player.

	// Draw the views
	function draw() 
	{
		canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
		canvas.strokeStyle = "red";
		canvas.strokeRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
		canvas.fillStyle = "#ccc";
		canvas.fillRect(.5, .5, CANVAS_WIDTH-1, CANVAS_HEIGHT-1);
		player.draw();
	}

The model animation data is maintained in the update function. There is the angle and the player x and y positions to update.

The player x value is the horizontal center of the canvas. That does not change in this example.

Line 57 computes the player y value. The values of 1 to -1 are computed from the angle using the Math.sin function. The angle variable increments infinitely. The Math.sin function uses the angle values as real possible angles in geometry you are accustomed to using.

The yRange value of 200 multiplied by Math.sin(angle) results in values between -200 and 200.

Adding the result to canvasCenterY, the vertical center of the canvas, creates the y value plus or minus the vertical center of the canvas.

	// Update the model data.
	function update() 
	{
		// Coordinate for x is center canvas width.
		player.x = canvasCenterX;
		// Coordinate y is the sine of the angle 
		// for a positive or negative percentage of the range 
		// using the canvas vertical for the center of the range.
		player.y = canvasCenterY + Math.sin(angle) * yRange;
		// Angle changed by speed.
		angle += speed;
	}

[ad name=”Google Adsense”]
This is the object that is being animated. It contains some basic properties for a circle and contains a method for drawing.

	var player = {
		color: "#00A",
		x: 0,  	// Starting x
		y: 0,	// Starting y
		width: 32,
		height: 32,
		radius: 20,
		draw: function() // Draw the player.
		{
			canvas.fillStyle = this.color;		
			canvas.beginPath();  
			canvas.arc(this.x,this.y,this.radius,0,Math.PI*2,true); 
			canvas.fill();
			canvas.lineWidth = 5;
    		canvas.strokeStyle = "black";
    		canvas.stroke();
	  	}
	};	

The entire HTML document for your convenience to view and copy.

<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<script language="javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js" type="text/javascript"></script>
<title>HTML5 Canvas Based Animation Sine Wave Movement Using Bobbing Circle</title>
</head>
<body>
<script type='text/javascript'>
	var CANVAS_WIDTH = 480;		// Canvas width
	var CANVAS_HEIGHT = 480;	// Canvas height
	var FPS = 30;				// Frames per second
	// Canvas center points adjusted for stroke.
	var canvasCenterX = (CANVAS_WIDTH - 1) / 2;
	var canvasCenterY = (CANVAS_HEIGHT - 1) / 2;
	// Speed of movement
	var speed = .05;
	// Angle for computing cosine and sin.
	var angle = 0;
	// Range of the vertical movement.
	var yRange = 200;
	// Create a canvas element variable.
	var canvasElement = $("<canvas width='" + CANVAS_WIDTH + 
	  "' height='" + CANVAS_HEIGHT + "'></canvas");
	// Reference to the canvas 2d context.
	var canvas = canvasElement.get(0).getContext("2d");
	// Dynamically append a canvas element to the body tag.
	canvasElement.appendTo('body');	
	// Object defining the player we are moving.
	var player = {
		color: "#00A",
		x: 0,  	// Starting x
		y: 0,	// Starting y
		width: 32,
		height: 32,
		radius: 20,
		draw: function() // Draw the player.
		{
			canvas.fillStyle = this.color;		
			canvas.beginPath();  
			canvas.arc(this.x,this.y,this.radius,0,Math.PI*2,true); 
			canvas.fill();
			canvas.lineWidth = 5;
    		canvas.strokeStyle = "black";
    		canvas.stroke();
	  	},
	};	
	// Update the model data.
	function update() 
	{
		// Coordinate for x is center canvas width.
		player.x = canvasCenterX;
		// Coordinate y is the sine of the angle 
		// for a positive or negative percentage of the range 
		// using the canvas vertical for the center of the range.
		player.y = canvasCenterY + Math.sin(angle) * yRange;
		// Angle changed by speed.
		angle += speed;
	}
	// Draw the views
	function draw() 
	{
		canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
		canvas.strokeStyle = "red";
		canvas.strokeRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
		canvas.fillStyle = "#ccc";
		canvas.fillRect(.5, .5, CANVAS_WIDTH-1, CANVAS_HEIGHT-1);
		player.draw();
	}
	// JQuery ready
	$(function() 
	{
		// Start here.
		// Timer for animation.
		setInterval(function() 
		{
			update();
			draw();
		}, 1000/FPS);
	});
</script>
</body>
</html>

Categories
Articles

HTML5 Canvas Circular Rotation Animation Example Using JQuery Hotkeys For User Interaction

This takes my first example animating a filled circle rotating around the center point to a user interaction level using JQuery Hotkeys. JQuery Hotkeys is a plug-in that lets you easily add and remove handlers for keyboard events anywhere in your code supporting almost any key combination using JQuery.

Foundation HTML5 Animation with JavaScript
Learn More

This version of the animation allows for start and stop playing. It also includes reversing the direction of rotation from clockwise to counter clockwise and back again. Finally I decided to allow increasing and decreasing the speed of rotation.

Another change from the previous example is to move as much relevant functionality as possible into the animated object called player. For example the player object is responsible for its own data computations and for interpreting key events.

The events in the animation are the timer and key events. The timer event calls the update and draw functions at the desired frame rate. The update and draw functions then call the same functions for all animation objects: we only have one animation object at this point.

Demo

Download

[ad name=”Google Adsense”]
This is the html5 head section. Line 4 brings in JQuery from the Google CDN. Line 5 references a local copy of JQuery Hotkeys. In this case the 0.7.9 minimized version.

The html structure is expanded to include elements for a main title, a subtitle and keyboard instructions for the animation. The internal style on lines 9 – 27 provide formatting for these new structural elements.

<!DOCTYPE HTML>
<html>
<head>
<script language="javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js" type="text/javascript"></script>
<script src="js/jquery.hotkeys-0.7.9.min.js"></script>
<title>HTML5 Canvas Based Animation Circular Movement Start, Stop, Reverse, Faster, Slower</title>
<meta charset="UTF-8">
<title>Untitled Document</title>
<style type="text/css">
	body {
		font-family:Arial, Helvetica, sans-serif;
		font-size:12px;
	}
	#titleDiv{
		font-family:Verdana, Geneva, sans-serif;
	}
	#titleHeadDiv{
		font-size:16px;
	}
	#titleSub1Div{
		font-size:14px;
	}
	#instructionsDiv{
		margin-top:10px;
		margin-bottom:5px;	
	}
</style>
</head>
<body>
<div id="titleDiv">
    <div id="titleHeadDiv">HTML5 Canvas Based Animation Circular Movement</div>
    <div id="titleSub1Div">Key Controlled Start, Stop, Reverse, Faster, Slower</div>
</div>
<div id="instructionsDiv">Keys: P=Play, S=Stop, R=Reverse, Up=Faster, Down=Slower</div>

<script type='text/javascript'>

To keep the code readable, lines 38 – 57 define constants.

Line 54 – 68 are dynamically appending the canvas element to the body element. Then the canvas variable on line 61 represents the drawing context for the code.

// Canvas width
	var CANVAS_WIDTH = 480;	
	// Canvas height	
	var CANVAS_HEIGHT = 480;	
	// Rotation direction constants
	var CLOCKWISE = 'clockwise';
	var COUNTER_CLOCKWISE = 'counter_clockwise';
	// Key name constants
	var KEY_NAME_PLAY = 'p';
	var KEY_NAME_STOP = 's';
	var KEY_NAME_REVERSE = 'r';
	var KEY_NAME_FASTER = "up";
	var KEY_NAME_SLOWER = "down";
	// Constants to match JQuery Hotkeys event value type.
	var KEY_TYPE_UP = "keyup";
	var KEY_TYPE_DOWN = "keydown";
	var FPS = 30;				// Frames per second
	// Canvas center points adjusting for stroke.
	var canvasCenterX = (CANVAS_WIDTH - 1) / 2;
	var canvasCenterY = (CANVAS_HEIGHT - 1) / 2;
	// Create a canvas element variable.
	var canvasElement = $("<canvas width='" + CANVAS_WIDTH + 
	  "' height='" + CANVAS_HEIGHT + "'></canvas");
	// Reference to the canvas 2d context.
	var canvas = canvasElement.get(0).getContext("2d");
	// Dynamically append a canvas element to the body tag.
	canvasElement.appendTo('body');	
	// Object defining the player we are rotating.

[ad name=”Google Adsense”]
The player object is the bulk of the code to study.

Lines 65 – 77 define a series of properties for the player object. The player simply rotates around a point.

This example aimed at making the animation objects more independent. For example the speed property allows an independent speed.

	var player = {
		color: "#00A",
		x: 0,  		// Starting x
		y: 0,		// Starting y
		radius: 20,	// Radius of the player
		direction: CLOCKWISE,	// CLOCKWISE or COUNTER_CLOCKWISE
		angle:0,				// Degrees of rotation for sin and cosine
		speed: .05,				// Pixels per frame
		speedChange: .001,		// Pixels per frame
		speedMax: .18,			// Pixels per frame
		speedMin: .005,			// Pixels per frame
		radiusRotation:200,		// Rotation circle radius
		isPlaying: false,		// True false state animating

The player draw method is called from the main animation loop. The method uses the player object’s properties to draw the player. One bare number for lineWidth could have been added to properties.

		draw: function() // Draw the player.
		{
			canvas.fillStyle = this.color;		
			canvas.beginPath();  
			canvas.arc(this.x,this.y,this.radius,0,Math.PI*2,true); 
			canvas.fill();
			canvas.lineWidth = 5;
    		canvas.strokeStyle = "black";
    		canvas.stroke();
	  	},

The init method for the player object computes the starting x and y positions. The main animation init function calls the player init method.

		init: function()
		{
			this.x = canvasCenterX + Math.cos(this.angle) * this.radiusRotation;
			this.y = canvasCenterY + Math.sin(this.angle) * this.radiusRotation;
		},

Handling keyboard input is central to this example. The main code captures the key input using JQuery Hotkeys. There the player object keyEvent method is called. It receives the keyType representing either a key down or key up state and the name of the key.

The key down state is not checked but is the state for the play, stop, faster and slower key names. The key down state causes JQuery Hotkeys to repeat the key while it is down. This is acceptable for the play and stop key although we might want to limit those to a key up only state as you will see we did for the reverse key. The faster and slower keys we want to accept repetition if they are held down as we would with an accelerator or brake.

For the reverse key, the key up state is used. If the key down state was used, the reverse key would repeat creating a chaotic result for direction.

		keyEvent: function(keyType, keyName)
		{
			// Set to playing state.
			if (keyName == KEY_NAME_PLAY)
			{
				this.isPlaying = true;
			}
			// Set to stopped state
			else if (keyName == KEY_NAME_STOP)
			{
				this.isPlaying = false;
			}
			// Increase speed if playing
			else if (keyName == KEY_NAME_FASTER && this.isPlaying)
			{
				this.speed += this.speedChange;
				this.speed = Math.min(this.speedMax, this.speed);
			}
			// Decrease speed if playing
			else if (keyName == KEY_NAME_SLOWER && this.isPlaying)
			{
				this.speed -= this.speedChange;
				this.speed = Math.max(this.speedMin, this.speed);
			}
			// Up state of the key.
			if (keyType == KEY_TYPE_UP)
			{
				// Change from clockwise to counter clockwise rotation
				if (keyName == KEY_NAME_REVERSE && this.direction == CLOCKWISE)
				{
					player.direction = COUNTER_CLOCKWISE;
				}
				// Change from counter clockwise rotation to clockwise rotation
				else if (keyName == KEY_NAME_REVERSE && this.direction == COUNTER_CLOCKWISE)
				{
					player.direction = CLOCKWISE;
				}
			}			
		},

The update method handles computation of the model data for the player object. Assuming the player is moving, identified by the isPlaying property, the new position on the rotation perimeter is computed using standard trigonometry.

Also the direction is handled by simply incrementing or decrementing the angle property.

		update: function()
		{
			// Update player data model if playing.
			if (this.isPlaying)
			{
				// Compute the triangle coordinates from the center of rotation
				player.x = canvasCenterX + Math.cos(this.angle) * this.radiusRotation;
				player.y = canvasCenterY + Math.sin(this.angle) * this.radiusRotation;
				// Keep moving the rotation clockwise.
				if (this.direction == CLOCKWISE)
				{
					this.angle += this.speed;
				}
				// Keep moving the rotation counter clockwise.
				else if(this.direction == COUNTER_CLOCKWISE)
				{
					this.angle -= this.speed;
				}
			}
		}
	};	

This is the main animation init function. The task is to initialize all components. The JQuery Hotkeys are initialized here. The player object has its own init method.

The JQuery Hotkeys for the game are defined here. This is the syntax to listen for key events. Notice the distinction for listening to key down and key up states for each key. As mentioned above the key down states repeat so were useful for all but the reverse key where the key up state was captured.

	// Initialize game
	function init()
	{
		// Set keys for play
		$(document).bind(KEY_TYPE_DOWN, KEY_NAME_PLAY, keyEvent); 
		$(document).bind(KEY_TYPE_DOWN, KEY_NAME_STOP, keyEvent); 
		$(document).bind(KEY_TYPE_UP, KEY_NAME_REVERSE, keyEvent); 	
		$(document).bind(KEY_TYPE_DOWN, KEY_NAME_FASTER, keyEvent); 
		$(document).bind(KEY_TYPE_DOWN, KEY_NAME_SLOWER, keyEvent); 
		player.init();
	}

[ad name=”Google Adsense”]
The init function registers the keyEvent function for the keys JQuery Hotkeys processes.

We do a bit of conversion of the received event information to make it more readable to our code by creating a key name literal from the keyCode event property.

The final step is to call the keyEvent method on our only player object and it handles the key information accordingly.

	// Evaluate key input
	function keyEvent(evt)
	{
		// Animation name for the key pressed assumed to be the char name.
		var keyName = String.fromCharCode(evt.keyCode).toLowerCase();
		// Up arrow key
		if (evt.keyCode == 38)
		{
			keyName = KEY_NAME_FASTER;	
		}
		// Down arrow key
		if (evt.keyCode == 40)
		{
			keyName = KEY_NAME_SLOWER;	
		}
		// Call animation object keyEvent methods.
		player.keyEvent(evt.type,keyName);
		
	}

The main update and draw functions are called with the animation timer.

The update function recomputes the model data. In this case there is just updating the player model data.

For the draw method, the canvas draw operations are included and then the player draw method is called.

The separation of these two allows for a different timing solution that would separate the processing timing of the data and the drawing.

	// Update the model data.
	function update() 
	{  
		// Call the animation object update events.
		player.update();
	}
	// Draw the views
	function draw() 
	{
		//Canvas object clear and draw background and border.
		canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
		canvas.strokeStyle = "red";
		canvas.strokeRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
		canvas.fillStyle = "#ccc";
		canvas.fillRect(.5, .5, CANVAS_WIDTH-1, CANVAS_HEIGHT-1);
		// Call the animation object update events.
		player.draw();
	}

This is were the animation starts. Once JQuery fires its ready function, the animation init method is called and then a timer interval is calls the update and draw functions to drive the animation.

	// JQuery ready
	$(function() 
	{
		// Start here.
		
		// Initialize components
		init();
		// Timer for animation.
		setInterval(function() 
		{
			update();
			draw();
		}, 1000/FPS);
			
	});

</script>
</body>
</html>

Categories
Articles

HTML5 Canvas Circular Rotation Animation Example

Started dabbling with the HTML5 canvas to create animations. This example animates a filled circle rotating around the center point. I kept the example as simple as possible while anticipating some of the coding design required for a more complex build.

A build on this example that accepts user keyboard input to control the animation play, start, stop, direction and speed is available in this follow up post.

Foundation HTML5 Animation with JavaScript
Learn More

One item that is more sophisticated is that the canvas tag is dynamically created in the Javascript code on lines 35 – 40. This is not necessary as the tag could be inserted into the html and referenced with an id.

Another is including JQuery from the Google CDN. Listening to the body onLoad method is a substitute.

Also I included the script tag after the html content. The script tag could go in the head section. Placing after the html in the body tag allows html content to load and style before the script code is parsed.

Demo

Source

This is the html5 head section. There is an internal style on lines 8 – 17 for the html in the document. Line 4 brings in JQuery from the Google CDN. Line 20 is the display title for the page.

<!DOCTYPE HTML>
<html>
<head>
<script language="javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js" type="text/javascript"></script>
<title>HTML5 Canvas Circular Movement Based Animation</title>
<meta charset="UTF-8">
<title>Untitled Document</title>
<style type="text/css">
	body {
		font-family:Arial, Helvetica, sans-serif;
		font-size:12px;
	}
	#titleDiv{
		font-family:Verdana, Geneva, sans-serif;
		font-size:16px;
	}
</style>
</head>
<body>
<div id="titleDiv">HTML5 Canvas Circular Movement Based Animation</div>
<script type='text/javascript'>

The animation key variables are definite and initialized here to keep as many bare numbers out of the code as possible.

The canvas has a border. So there is some computational needs on lines 26-27 to take the border out in computing the center point. May not make a difference in this example, but is helpful for a future example where that boundary is important.

	var CANVAS_WIDTH = 480;		// Canvas width
	var CANVAS_HEIGHT = 480;	// Canvas height
	var FPS = 30;				// Frames per second
	// Canvas center points adjusting for stroke.
	var canvasCenterX = (CANVAS_WIDTH - 1) / 2;
	var canvasCenterY = (CANVAS_HEIGHT - 1) / 2;
	// Radius of the rotation
	var radius = 200;
	// Speed of movement
	var speed = .05;
	// Angle for computing cosine and sin.
	var angle = 0;
	// Create a canvas element variable.
	var canvasElement = $("<canvas width='" + CANVAS_WIDTH + 
	  "' height='" + CANVAS_HEIGHT + "'></canvas");
	// Reference to the canvas 2d context.
	var canvas = canvasElement.get(0).getContext("2d");
	// Dynamically append a canvas element to the body tag.
	canvasElement.appendTo('body');

[ad name=”Google Adsense”]
This defines the object that moves in the circular rotation. The name player is arbitrary but hints at a way to define objects for a game.

Later we will see the animation loop. The animation loop call the draw function of the items in the animation. So you see the draw function for the player on line 49. This is how to draw a filled circle with a border using the html5 canvas api.

Additionally each object has an init function that is called to initialize the animation objects. For this example the init function for the player object is empty. It could be used for an initial placement of an object for example.

Line 59 shows the master init function that would call all necessary component init functions.

	
	// Object defining the player we are rotating.
	var player = {
		color: "#00A",
		x: 0,  	// Starting x
		y: 0,	// Starting y
		width: 32,
		height: 32,
		radius: 20,
		draw: function() // Draw the player.
		{
			canvas.fillStyle = this.color;		
			canvas.beginPath();  
			canvas.arc(this.x,this.y,this.radius,0,Math.PI*2,true); 
			canvas.fill();
			canvas.lineWidth = 5;
    		        canvas.strokeStyle = "black";
    		        canvas.stroke();
	  	},
		init: function()
		{
		}
	};	
	// Initialize game
	function init()
	{
		player.init();
	}

[ad name=”Google Adsense”]
The update function updates the model data. The animation timer will call this for each animation interval.

Primarily the computations are a right triangle from the center point to the place for the player object to be drawn based on the current angle. This is the x and y where the leg and hypotenuse are based on the angle.

	// Update the model data.
	function update() 
	{  
		// Compute the triangle coordinates from the center of rotation
		player.x = canvasCenterX + Math.cos(angle) * radius;
		player.y = canvasCenterY + Math.sin(angle) * radius;
		// Keep moving the rotation. Use -= for reverse rotation.
		angle += speed;
	}

This is the view being drawn. It draws anything that has to do with the overall canvas. However you could make that a class or object like player and give it a draw method.

Line 85 shows the player draw method being called.

	// Draw the views
	function draw() 
	{
		canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
		canvas.strokeStyle = "red";
		canvas.strokeRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
		canvas.fillStyle = "#ccc";
		canvas.fillRect(.5, .5, CANVAS_WIDTH-1, CANVAS_HEIGHT-1);
		player.draw();
	}

[ad name=”Google Adsense”]
This is the start of the script and the heart of the animation. Once JQuery is ready the animation init function is called. Next a Javascript interval is started to call the animation update method to update the data and then the animation draw method to redraw the view.

	// JQuery ready
	$(function() 
	{
		// Start here.
		init();
		// Timer for animation.
		setInterval(function() 
		{
			update();
			draw();
		}, 1000/FPS);
			
	});

</script>
</body>
</html>

Categories
Articles

XCode 4 IPhone Mountains of the USA Tutorial: Lesson 9 – Add Annotation to MapView


<== Lesson 8 || Overview ||

This lesson adds an annotation and pin on the map to better show the location of the mountain.

Detail View with MapView Annotation

MapAnnotation class is all that you need to get a pin on the map with an annotation above it.

However, you may want the annotation and pin to “drop in” or you might want to control the pin color. This requires a MKMapViewDelegate class and the viewForAnnotation method. In this method a MKPinAnnotationView object is created to embellish the annotation with more functionality as well as the “drop in” effect. The DetailViewController class will serve as the MKMapViewDelegate.

As you proceed through the steps, the code items not needed to “just show an annotation and pin” are identified for you so you can try an example without the special drop in effect. To summarize that approach in advance, you do not need to add the MKMapViewDelegate protocol to your DetailViewController.h file, you exclude the [mapView setDelegate:self]; line from the viewWillAppear method in the DetailViewContoller.m files and you will not need the viewForAnnotation method added in step 3.

Source Download

  1. Starting XCode 4 Project. This is the lesson 8 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: DetailViewController.h – Add the MKMapViewDelegate Protocol
Download and uncompress the Starting XCode Project file and open in XCode.

Open the DetailViewController.h class and add line 3 to include the MapAnnotation header.

On line 6 you need to edit in the MKMapViewDelegate protocol. This line is needed only if you want more functionality over the annotation such as pin color or a “drop in” effect by including the viewForAnnotation method.

#import &amp;amp;lt;UIKit/UIKit.h&amp;amp;gt;
#import &amp;amp;lt;MapKit/Mapkit.h&amp;amp;gt;
#import "MapAnnotation.h"
#import "MountainItem.h"

@interface DetailViewController : UIViewController &amp;amp;lt;MKMapViewDelegate&amp;amp;gt;{
    MKMapView *mapView;

    MountainItem *mountainItem;
}

@property (nonatomic, retain) IBOutlet MKMapView *mapView;

@property (nonatomic, retain) MountainItem *mountainItem;

@end

Step 2: DetailViewController.m – Create the Annotation
Open the DetailViewController.m file.

The first 51 lines of code are not changing but included here for online reference:

#import "DetailViewController.h"

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

- (void)dealloc
{
    [mapView 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.mapView = nil;

}

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

Add the highlighed line in the next code view.

Line 68 sets this class to receive MKMapViewDelegate messages. In the last step you defined this class as MKMapViewDelegate.

Line 68 is not needed if all you want to show the annotation and pin without any special view or effects. A MKMapViewDelegate is not needed for that.

Line 70 clears all annotations from the map. You can leave this line out and the previous pins and their annotations will stay on the map. If you try some mountains in the same range or zoom way out, you will see the previous pins and when you touch the pin the annotation will appear.

Lines 71 to 73 create the annotation object, give it a title and position in the center of the view region created in the last lesson on lines 61 to 66.

The annotation is added to the map on line 75.

The annotation is selected on line 77. When you test the app, you should touch the annotation and the pin. You will see the selected and unselected state. Unselected, just a pin appears. Selected the title appears. Line 77 is achieving the selection process in code.

Line 78 is clean up.

-(void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self.navigationController setNavigationBarHidden:NO animated:YES];
    [self setTitle:mountainItem.name];

    [mapView setMapType:MKMapTypeStandard];
    [mapView setZoomEnabled:YES];
    [mapView setScrollEnabled:YES];
    MKCoordinateRegion region = { {0.0, 0.0 }, { 0.0, 0.0 } };
    region.center.latitude = [mountainItem.latitude doubleValue] ;
    region.center.longitude = [mountainItem.longitude doubleValue] ;
    region.span.longitudeDelta = 1.0f;
    region.span.latitudeDelta = 1.0f;
    [mapView setRegion:region animated:YES];

    [mapView setDelegate:self];

    [self.mapView removeAnnotations:self.mapView.annotations];
    MapAnnotation *ann = [[MapAnnotation alloc] init];
    ann.title = mountainItem.name;
    ann.coordinate = region.center;

    [mapView addAnnotation:ann];

    [mapView selectAnnotation:ann animated:YES];
    [ann release];
}

[ad name=”Google Adsense”]

Step 3: DetailViewController.m – Add Annotation View and Drop In Effect

The viewForAnnotation method is called to develop the view for an annotation.

This implementation is simple because we only have one annotation. The method is called for all annotations. With multiple annotation, you may need to treat certain annotations differently, so you might need to identify which is calling this method. As well if you are using a unpredictable number of annotations and some annotations persist, this is a place to determine how to reuse MKPinAnnotationView objects. All of this is beyond the scope of the tutorial, but this explains why you will see more lines of code in other examples and why this example appears simpler.

Line 83 creates the MKPinAnnotationView annView object. You are going to reuse the same MKPinAnnotationView, so the identifier MyPin is created to help that happen.

The pin “drop in” effect occurs because of line 84.

Line 85 is needed to show the pin annotation title. There are more items that can be added to the annotation such as a subtitle and left and right views.

The pin color is set on line 87. You can use MKPinAnnotationColorGreen and MKPinAnnotationColorPurple. These and MKPinAnnotationColorRed were added in IOS 3.0.

- (MKAnnotationView *) mapView:(MKMapView *)mapView viewForAnnotation:(id &amp;amp;lt;MKAnnotation&amp;amp;gt;) annotation

{
    MKPinAnnotationView *annView = [[[MKPinAnnotationView alloc ] initWithAnnotation:annotation reuseIdentifier:@"MyPin"] autorelease] ;
    annView.animatesDrop=TRUE;
    annView.canShowCallout = YES;

	annView.pinColor = MKPinAnnotationColorRed;

    return annView;
}

Try in the simulator. Tap the annotations and the pins to get the feel for them.

Categories
Articles

XCode 4 IPhone Mountains of the USA Tutorial: Lesson 8 – Add MapView


<== Lesson 7 || Overview || Lesson 9 ==>

Now you have the second view added in the last lesson, you can add a map to show where the mountain is located. You can use the longitude and latitude that are part of the MountainItem data passed to the second view.

Detail View Screen with MapView

To work with a map, you need to include the MapKit framework. There are frameworks included automatically with a new IOS XCode project. They are UIKit, Foundation and CoreGraphics. You have probably noticed the import statement for UIKit in the header for MainViewController and DetailViewController.

In XCode 4 a group called Frameworks contains the frameworks you include with your project.

Typical Frameworks Group

It is a good practice to assure any new ones you add are in that group for easy reference.

Source Download

  1. Starting XCode 4 Project. This is the lesson 7 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: Add the MapKit Framework
Download and uncompress the Starting XCode Project file and open in XCode.

In the project explorer select the top node.

Select Project Node

In the center of XCode follow these steps:

Steps to Open Frameworks and Libraries Dialog

Type map into the search text field and you should see the list narrow down to the MapKit.framework choice and then select the Add button.

Frameworks and Libraries Dialog

Locate the MapKit.framework file in the project explorer and drag it into the Frameworks group. You could leave MapKit.framework where it was grouped, but keeping the frameworks together makes sense.

Drag MapKit Framework To Frameworks Group

Step 2: DetailViewController.h – Replace TextView with MapView

Open the DetailViewController.h file in the project explorer.

You are going to replace the UITextView with a MKMapView. Lines that are being replaced are included here for convenience.

#import &amp;lt;UIKit/UIKit.h&amp;gt;
#import "MountainItem.h"

@interface DetailViewController : UIViewController {
    UITextView *mountainInfoTextView;

    MountainItem *mountainItem;
}
@property (nonatomic, retain) IBOutlet UITextView *mountainInfoTextView;

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

Line 2 below show the inclusion of MapKit in this class.

Lines 6 and 11 give you an IBOutlet to the MapView you add later to the DetailViewController.xib.

#import &amp;lt;UIKit/UIKit.h&amp;gt;
#import &amp;lt;MapKit/Mapkit.h&amp;gt;
#import "MountainItem.h"

@interface DetailViewController : UIViewController &amp;lt;MKMapViewDelegate&amp;gt;{
    MKMapView *mapView;

    MountainItem *mountainItem;
}

@property (nonatomic, retain) IBOutlet MKMapView *mapView;

@property (nonatomic, retain) MountainItem *mountainItem;

@end

Step 3: DetailViewController.m – Add mapView Property to the Implementation

Open the DetailViewController.m file in the project explorer.

These are the standard lines to include the mapView property to the implementation file.

You need to remove the mountainInfoTextView property and for convenience the lines are shown here.

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

Include the lines below to add the mapView object.

#import "DetailViewController.h"

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

- (void)dealloc
{
    [mapView 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.mapView = nil;

}

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

[ad name=”Google Adsense”]
Step 4: DetailViewController.m – Show the Mountain’s Location on the Map

Remove line 58 because you are replacing the mountainInfoTextView with the coding for the mapView.

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

Add the highlighted lines in the next code view.

The standard map type is set on line 59. You could also use MKMapTypeSatellite and MKMapTypeHybrid if you want to experiment.

Lines 60 and 61 are self explanatory.

The zoom and center point of the map is done on lines 62 to 67. Then center point is the longitude and latitude data passed into the class in the mountainItem object.

The span is an offset for the zoom. You can experiment with positive float numbers such as .01 which will zoom in tight. Because many of the mountains do not have any feature in standard map view, the value of 1 seems to show enough map details to keep from being disoriented.

-(void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self.navigationController setNavigationBarHidden:NO animated:YES];
    [self setTitle:mountainItem.name];

    [mapView setMapType:MKMapTypeStandard];
    [mapView setZoomEnabled:YES];
    [mapView setScrollEnabled:YES];
    MKCoordinateRegion region = { {0.0, 0.0 }, { 0.0, 0.0 } };
    region.center.latitude = [mountainItem.latitude doubleValue] ;
    region.center.longitude = [mountainItem.longitude doubleValue] ;
    region.span.longitudeDelta = 1.0f;
    region.span.latitudeDelta = 1.0f;
    [mapView setRegion:region animated:YES];

}

@end

Step 5: DetailViewController.xib – Replace the TextView with the MapView

Open the DetailViewController.xib.

Delete the TextView in the design window.

Delete TextView

Drag the MapView from the Objects panel to the view in the design window and fill the view.

Drag MapView

With the MapView still selected open the Connections Inspector and drag a “New Referencing Outlet” to the File’s Owner and when you release the mouse select mapView, the name your are using in our DetailViewController to control the MapView. The Connections Inspector should look as follows:

Connections Inspector For MapView

Then finally as a double check select the File’s Owner and the Connections Inspector should appear as follows:

Connections Inspector for File’s Owner

You should be good to check this out in the Simulator. Remember you need to hold down the Option button and drag the mouse to get the multi touch over the map for zooming. Scroll the map by dragging the mouse.

<== Lesson 7 || Overview || Lesson 9 ==>

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