Aplicación iOS y Google App Engine

Cliente de iPhone

Tenemos la parte del servidor lista para recibir peticiones y proveer la información relacionada con los conciertos que mantiene en su base de datos. Ahora el siguiente paso es crear ese cliente que se conectará y mostrará estos datos de la forma típica en aplicaciones iPhone.

Este tutorial constará de 6 partes principales:

- Clase Concierto.

- Crear un parser del XML de los conciertos.

- Clase principal que contendrá un UINavigationControllerView

- Vista UITableView donde aparecerá la lista de conciertos.

- Vista detallada de un concierto.

- Vista para reservar entradas del concierto.

Pero para empezar todo esto, lo primero es crear un nuevo proyecto del tipo Navigation-Based Application. Como nombre le he puesto “XML”. Ahora ya podemos seguir el guión anterior.

1. Crear la clase Concierto:

Concierto.h

#import <Foundation/Foundation.h>

@interface Concierto : NSObject {
	NSInteger cID;
	NSString *artista;
	NSString *fecha;
	int entradas;
	int vendidas;
}

@property (nonatomic, readwrite) NSInteger cID;
@property (nonatomic, retain) NSString *artista;
@property (nonatomic, retain) NSString *fecha;
@property (nonatomic) int entradas;
@property (nonatomic) int vendidas;

@end

Concierto.m

#import "Concierto.h"

@implementation Concierto

@synthesize cID, artista, fecha, entradas, vendidas;

- (void) dealloc {
	[artista release];
	[fecha release];
	[super dealloc];
}

@end

2. XMLParser:

Crear la clase que hará de parser del XML que recibiremos como respuesta a nuestra peticion al servidor, pero antes tenemos que añadir una colección de Conciertos en AppDelegate.

XMLAppDelegate.h

#import <Foundation/Foundation.h>

@interface XMLAppDelegate : NSObject {

    UIWindow *window;
    UINavigationController *navigationController;

	NSMutableArray *concerts;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UINavigationController *navigationController;

@property (nonatomic, retain) NSMutableArray *concerts;

@end

En este punto ya podemos centrarnos en la clase que a partir del XML con la información de los conciertos, creará un objeto Concierto por cada elemento y lo añadirá a la coleccion NSMutableArray de XMLAppDelegate.

Creamos una nueva clase XMLParser:

XMLParser.h

#import <Foundation/Foundation.h>

@class XMLAppDelegate, Concierto;

@interface XMLParser : NSObject {

	NSMutableString *currentElementValue;

	XMLAppDelegate *appDelegate;
	Concierto *aConcert;
}

- (XMLParser *) initXMLParser;

@end

XMLParser.m, la implementación de esta clase es más la parte más complicada de la aplicación por lo que veremos los métodos de uno en uno:

- (XMLParser *) initXMLParser {

	[super init];

	appDelegate = (XMLAppDelegate *)[[UIApplication sharedApplication] delegate];

	return self;
}

En esta clase debemos implementar 3 métodos delegados:

  • didStartElement
  • foundCharacters
  • didEndElement
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName
  namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName
	attributes:(NSDictionary *)attributeDict {

	if([elementName isEqualToString:@"conciertos"]) {
		//Initialize the array.
		appDelegate.concerts = [[NSMutableArray alloc] init];
	}
	else if([elementName isEqualToString:@"concierto"]) {

		//Initialize the concert.
		aConcert = [[Concierto alloc] init];

		//Extract the attribute here.
		aConcert.cID = [[attributeDict objectForKey:@"id"] integerValue];

		NSLog(@"Reading id value :%i", aConcert.cID);
	}

	NSLog(@"Processing Element: %@", elementName);
}
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string { 

	if(!currentElementValue)
		currentElementValue = [[NSMutableString alloc] initWithString:string];
	else
		[currentElementValue appendString:string];

	NSLog(@"Processing Value: %@", currentElementValue);

}
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName
  namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {

	if([elementName isEqualToString:@"conciertos"])
		return;

	//There is nothing to do if we encounter the Books element here.
	//If we encounter the Book element howevere, we want to add the book object to the array
	// and release the object.
	NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
	[formatter setNumberStyle:NSNumberFormatterDecimalStyle];

	if([elementName isEqualToString:@"concierto"]) {
		[appDelegate.concerts addObject:aConcert];

		[aConcert release];
		aConcert = nil;
	}
	else {
		NSString *trimmed = [currentElementValue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];

		if([elementName isEqualToString:@"artista"] || [elementName isEqualToString:@"fecha"]
						|| [elementName isEqualToString:@"urlImagen"]) {
			[aConcert setValue:trimmed forKey:elementName];
		} else {

			int value = [[formatter numberFromString:trimmed] intValue];
			if ([elementName isEqualToString:@"entradas"]) {
				aConcert.entradas = value;
			} else if ([elementName isEqualToString:@"vendidas"]){
				aConcert.vendidas = value;
			}
		}

	}

	[formatter release];
	[currentElementValue release];
	currentElementValue = nil;
}

3. Realizar el parse del XML:

Cuando nuestra applicación acaba de ser lanzada, se ejecuta el metodo applicationDidFinishLaunching de XMLAppDelegate. Es aquí donde primero realizaremos la conexión al servidor para solicitar la respuesta con formato XML y luego parsearla tal y como se muestra en el código:

- (void)applicationDidFinishLaunching:(UIApplication *)application {

	NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
	[request setURL:[NSURL URLWithString:@"http://iphone-concerts-app.appspot.com/conciertos"]];
	[request setHTTPMethod: @"POST"];

	NSError *error;
	NSURLResponse *response;
	NSData* result = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];

	NSLog(@"%@", result);

	NSXMLParser *xmlParser = [[NSXMLParser alloc] initWithData:result];

	//Initialize the delegate.
	XMLParser *parser = [[XMLParser alloc] initXMLParser];

	//Set delegate
	[xmlParser setDelegate:parser];

	//Start parsing the XML file.
	BOOL success = [xmlParser parse];

	if(success)
		NSLog(@"No Errors");
	else
		NSLog(@"Error Error Error!!!");

	// Configure and show the window
	[window addSubview:[navigationController view]];
	[window makeKeyAndVisible];
}

4. RootViewController:

Ya tenemos la información de los conciertos almacenada en un NSMutableArray. Ahora debemos mostrarla en RootViewController, que es la vista de UITableView.

RootViewController.h

#import <UIKit/UIKit.h>

@class XMLAppDelegate, ConcertDetailViewController;

@interface RootViewController : UITableViewController {

	XMLAppDelegate *appDelegate;
	ConcertDetailViewController *concertController;
}

@end

RootViewController.m

Como nuestra clase hereda de UITableViewController, tendremos que implementar los siguientes métodos:

· numberOfSectionsInTable

· numberOfRowsInSection

· cellForRowAtIndexPath

· didSelectRowAtIndexPath

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [appDelegate.concerts count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
    }

	Concierto *aConcert = [appDelegate.concerts objectAtIndex:indexPath.row];

	cell.text = aConcert.artista;
	cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;

    // Set up the cell
    return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    // Navigation logic -- create and push a new view controller

	if(concertController == nil)
		concertController = [[ConcertDetailViewController alloc] initWithNibName:@"ConcertDetailView" bundle:[NSBundle mainBundle]];

	Concierto *aConcert = [appDelegate.concerts objectAtIndex:indexPath.row];

	concertController.aConcert = aConcert;

	[self.navigationController pushViewController:concertController animated:YES];
}

y para terminar el método que se ejecuta al cargar la vista:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Uncomment the following line to add the Edit button to the navigation bar.
    // self.navigationItem.rightBarButtonItem = self.editButtonItem;

	appDelegate = (XMLAppDelegate *)[[UIApplication sharedApplication] delegate];

	self.title = @"Conciertos";
}

5. Detalles de un concierto:

Ahora queremos mostrar todos los datos tenemos del concierto seleccionado. Utilizaremos un UITableView para los detalles y un UIButton que nos llevará a la siguiente vista de reservas.

Captura de pantalla 2010 11 04 a las 18.21.12 300x255 Aplicación iOS y Google App Engine

ConcertDetailViewController.h

#import <UIKit/UIKit.h>
#import "ReservasViewController.h"

@class Concierto;

@interface ConcertDetailViewController : UIViewController {

	IBOutlet UITableView *tableView;

	Concierto *aConcert;
	ReservasViewController *reservasController;
}

@property (nonatomic, retain) Concierto *aConcert;
@property (nonatomic, retain) ReservasViewController *reservasController;

-(IBAction)reservar:(id)sender;

@end

ConcertDetailViewController.m

#import "ConcertDetailViewController.h"
#import "Concierto.h"

@implementation ConcertDetailViewController

@synthesize aConcert;
@synthesize reservasController;
@synthesize image;

// Implement viewDidLoad to do additional setup after loading the view.
- (void)viewDidLoad {
    [super viewDidLoad];

	self.title = @"Detalles";
}

- (void)viewWillAppear:(BOOL)animated {
	[super viewWillAppear:animated];

	[tableView reloadData];
}

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

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning]; // Releases the view if it doesn't have a superview
    // Release anything that's not essential, such as cached data
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 3;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 1;
}

- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
    }

	switch(indexPath.section)
	{
		case 0:
			cell.text = aConcert.artista;
			break;
		case 1:
			cell.text = aConcert.fecha;
			break;
		case 2:
			cell.text = [NSString stringWithFormat:@"%d/%d", aConcert.vendidas, aConcert.entradas];
			break;
	}

	return cell;
}

- (NSString *)tableView:(UITableView *)tblView titleForHeaderInSection:(NSInteger)section {

	NSString *sectionName = nil;

	switch(section)
	{
		case 0:
			sectionName = [NSString stringWithString:@"Artista"];
			break;
		case 1:
			sectionName = [NSString stringWithString:@"Fecha"];
			break;
		case 2:
			sectionName = [NSString stringWithString:@"Entradas (vendidas/totales)"];
			break;
	}

	return sectionName;
}

- (void)dealloc {

	[aConcert release];
	[tableView release];
	[reservasController release];
    [super dealloc];
}

-(IBAction)reservar:(id)sender {
	if(reservasController == nil)
		reservasController = [[ReservasViewController alloc] initWithNibName:@"ReservasViewController" bundle:[NSBundle mainBundle]];

	reservasController.aConcert = self.aConcert;

	[self.navigationController pushViewController:reservasController animated:YES];
}

@end

6. Reservar:

Ya estamos en el paso final, las reservas. En la interfaz mostraremos un pequeño resumen de los datos del concierto (para asegurarnos que es el que queremos) usando UILabel, un UITextField para introducir cuantas entradas queremos y por último un UIButton para enviar la solicitud de reservas al servidor.

Captura de pantalla 2010 11 04 a las 18.38.31 206x300 Aplicación iOS y Google App Engine

ReservasViewController.h

#import "Concierto.h"

@interface ReservasViewController : UIViewController {
	Concierto *aConcert;

	IBOutlet UILabel *labelArtista;
	IBOutlet UILabel *labelFecha;
	IBOutlet UILabel *labelDisponibles;
	IBOutlet UITextField *fieldReservar;
}

@property (nonatomic, retain) Concierto *aConcert;
@property (nonatomic, retain) IBOutlet UILabel *labelArtista;
@property (nonatomic, retain) IBOutlet UILabel *labelFecha;
@property (nonatomic, retain) IBOutlet UILabel *labelDisponibles;
@property (nonatomic, retain) IBOutlet UITextField *fieldReservar;

-(IBAction)confirmarReserva:(id)sender;

@end

ReservasViewController.m

#import "ReservasViewController.h"

@implementation ReservasViewController

@synthesize aConcert;
@synthesize labelArtista, labelFecha, labelDisponibles, fieldReservar;

- (void)viewDidAppear:(BOOL)animated {
	self.title = @"Reservar";

	labelArtista.text = aConcert.artista;
	labelFecha.text = aConcert.fecha;
	labelDisponibles.text = [NSString stringWithFormat:@"%d", aConcert.entradas - aConcert.vendidas];

    [super viewDidLoad];
}

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

- (void)viewDidUnload {
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}

- (void)dealloc {
	[aConcert release];
	[labelArtista release];
	[labelFecha release];
	[labelDisponibles release];
	[fieldReservar release];
    [super dealloc];
}

- (IBAction)confirmarReserva:(id)sender {
	NSString *stringReservas = fieldReservar.text;
	NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
	[formatter setNumberStyle:NSNumberFormatterDecimalStyle];
	int reservadas = [[formatter numberFromString:stringReservas] intValue];

	if (reservadas != 0) {
		if (reservadas > (aConcert.entradas - aConcert.vendidas) ) {
			UIAlertView* alertView = [[UIAlertView alloc] initWithTitle:@"Centro de reservas" message:@"No hay tantas entradas disponibles." delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
			[alertView show];
			[alertView release];
		} else {
			aConcert.vendidas += reservadas;
			fieldReservar.text = nil;

			UIAlertView* alertView = [[UIAlertView alloc] initWithTitle:@"Centro de reservas" message:@"Entradas reservadas con éxito." delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
			[alertView show];
			[alertView release];

			[self.navigationController popViewControllerAnimated:YES];

			//enviar al servidor el id y num de entradas reservado
			NSString *myRequestString = [NSString stringWithFormat:@"id=%d&vendidas=%d", aConcert.cID, reservadas];
			NSData *myRequestData = [NSData dataWithBytes: [myRequestString UTF8String] length: [myRequestString length]];

			NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
			[request setURL:[NSURL URLWithString:@"http://iphone-concerts-app.appspot.com/update"]];
			[request setHTTPMethod: @"POST"];;
			[request setHTTPBody: myRequestData];

			NSError *error;
			NSURLResponse *response;
			NSData* result = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];

		}
	}

}

@end

Páginas: 1 2 3