segunda-feira, 24 de setembro de 2012

Mapas no iOS

Olá povo,

Esse post é grande, mas é bem legal. Nele, vou falar sobre a utilização de Mapas no iOS. Conheceremos as principais classes do MapKit, e além desse objetivo principal, utlizaremos um conceito interessante do Objective-C, os blocks.
Para começar crie um novo projeto no Xcode (pode ser Single View Application sem utilizar Storyboards). Adicione os frameworks MapKit e CoreLocation à sua aplicação. Em seguida, crie uma nova classe chamada Endereço e deixe-a conforme abaixo:
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>

@interface Endereco : NSObject<MKAnnotation>

@property (strong, nonatomic) NSString *titulo;
@property (strong, nonatomic) NSString *subtitulo;

- (id)initWithCoordinate:(CLLocationCoordinate2D) c;
@end
#import "Endereco.h"

@implementation Endereco

@synthesize titulo, subtitulo;

@synthesize coordinate;

- (id)initWithCoordinate:(CLLocationCoordinate2D) c {
    coordinate = c;
    NSLog(@"%f,%f", c.latitude, c.longitude);
    return self;
}

- (NSString *)subtitle {
    return subtitulo;
}

- (NSString *) title {
    return titulo;
}
@end
A classe acima está implementando o protocolo MKAnnotation que exige que exista uma propriedade coordinate e os métodos subtitle e title. Isso porque, usaremos essa classe para representar um ponto no Mapa.
Agora deixe o ViewController da sua aplicação conforme abaixo (o nome da classe obviamente pode ser alterado...).
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#import "Endereco.h"

@interface NGViewController : UIViewController 
  <UISearchBarDelegate, MKMapViewDelegate>

@property (weak, nonatomic) IBOutlet 
  UISearchBar *searchBar;
@property (weak, nonatomic) IBOutlet 
  MKMapView *mapa;
@property (weak, nonatomic) IBOutlet 
  UISegmentedControl *segMapType;
@property (strong, nonatomic) 
  Endereco *endereco;

- (IBAction)mapViewChanged:(id)sender;

@end
#import "NGViewController.h"
#import "Endereco.h"

@implementation NGViewController

@synthesize searchBar;
@synthesize mapa;
@synthesize segMapType;
@synthesize endereco;

#define API_KEY @"SUA_CHAVE"
#define TEMPLATE_URL 
  @"http://maps.google.com/maps/geo?q=%@&output=csv&key=%@"

#pragma mark - View lifecycle
- (void)viewDidUnload {
    [self setSearchBar:nil];
    [self setMapa:nil];
    [self setSegMapType:nil];
    [super viewDidUnload];
}

#pragma mark - SegmentedControl value changed
- (IBAction)mapViewChanged:(id)sender {
  // De acordo com a posição selecionada no 
  // SegmentedControl, mudamos o tipo de 
  // visualização do mapa
  switch ([segMapType selectedSegmentIndex]) {
    case 0: mapa.mapType = MKMapTypeStandard;  break;
    case 1: mapa.mapType = MKMapTypeSatellite; break;
    case 2: mapa.mapType = MKMapTypeHybrid;    break;
  }
}

#pragma mark - SearchBar methods
-(void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
  // Ao clicar em cancelar na barra de busca, 
  // ocultamos o teclado
  [self.searchBar resignFirstResponder];
}

- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
  [self.searchBar resignFirstResponder];    
    
  [self retrieveLocation:self.searchBar.text];
}

#pragma mark - Utility Methods

- (void) retrieveLocation:(NSString *)search {
  // Criando URL de requisição
  NSString *query = 
    [search stringByAddingPercentEscapesUsingEncoding:
      NSUTF8StringEncoding]; 
   
  NSString *urlString = [NSString stringWithFormat:
    TEMPLATE_URL, query, API_KEY];
    
  // Preparando request
  NSURLRequest *request = [[NSURLRequest alloc]
    initWithURL:[NSURL URLWithString:urlString]];
    
  // Declarando o Block "trataResposta" para tratar 
  // retorno. Esse block é uma função que retorna 
  // void e recebe 3 parâmetros: uma response, os 
  // dados e um erro. 
  id trataResposta = ^(NSURLResponse *response, 
    NSData *data, NSError *error) {
      if (error){
        [self showErrorDialog];
            
      } else {
        [self trataResposta:data];
      }
    };
    
  // Fazendo a requisição assícrona para evitar que 
  // a tela pareça estar travada. Aqui usamos o 
  // recurso de blocks, declarado acima e que será 
  // chamado após a requisição ser concluída
  [NSURLConnection sendAsynchronousRequest:request 
    queue:[NSOperationQueue mainQueue] 
      completionHandler:trataResposta];
}

- (void) trataResposta:(NSData *)data {
  NSString *locationString = [[NSString alloc]
    initWithData:data encoding:NSUTF8StringEncoding];

  // Nesse array, o primeiro elemento é o código de 
  // resposta HTTP (200 é sucesso) os próximos (se 
  // deu tudo certo) é a latitude e longitude.
  NSArray *listItems = [locationString 
    componentsSeparatedByString:@","];
    
  double longitude = 0;
  double latitude = 0;
    
  if ([listItems count] > 0 && 
     [[listItems objectAtIndex:0] 
       isEqualToString:@"200"]){

    latitude = [[listItems objectAtIndex:2] 
      doubleValue];
    longitude = [[listItems objectAtIndex:3] 
      doubleValue];
  }
    
  // Cria o objeto location fazer obter informações 
  // do local via GeoCoder
  CLLocation *location = [[CLLocation alloc] 
    initWithLatitude:latitude longitude:longitude];
  [self retrieveGeocoder:location];
}

- (void) retrieveGeocoder:(CLLocation *)location {
  CLGeocoder *geocoder = [[CLGeocoder alloc] init];
    
  // Mais uma chamada assícrona que recebe 
  // um block como parâmetro
  [geocoder reverseGeocodeLocation:location 
    completionHandler:
      ^(NSArray *placemarks, NSError *error) {
         
    if ([placemarks count] > 0){
             
      CLPlacemark *placemark = 
        [placemarks objectAtIndex:0];
             
      [self addAnnotation:location.coordinate 
         withMark:placemark];
             
    } else {
      [self showErrorDialog];
    }
  }];
}

- (void)showErrorDialog {
  UIAlertView *alert = [[UIAlertView alloc] 
    initWithTitle:@"Erro" 
    message:@"Erro ao obter dados da localização" 
    delegate:nil cancelButtonTitle:@"OK" 
    otherButtonTitles:nil, nil];
  [alert show];
}

- (void)addAnnotation:
  (CLLocationCoordinate2D)coordinate 
   withMark:(CLPlacemark *)placemark {

  // Se já houver uma marcação no mapa, remova
  if (self.endereco != nil) {
    [mapa removeAnnotation:self.endereco];
    self.endereco = nil;
  }
    
  // Criando MKNotation para adicionar no Mapa
  self.endereco = [[Endereco alloc]
  initWithCoordinate:coordinate];
    
  // Configura o título e subtítulo da MKNotation 
  // baseado no CLPlacemark
  self.endereco.titulo = [NSString stringWithFormat:
    @"%@, %@\n%@", 
      placemark.locality, 
      placemark.administrativeArea, 
      placemark.postalCode];
    
  self.endereco.subtitulo = 
    [NSString stringWithFormat:@"%@, %f,%f", 
      placemark.thoroughfare,
      coordinate.latitude, 
      coordinate.longitude];
    
  // Adiciona a notação ao mapa
  [self.mapa addAnnotation:self.endereco];

  /* A MKCoordinateSpan representa a quantidade 
     de área usada pela view e é usada para 
     determinar o zoom. Quanto maior o valor, 
     maior a área visível. */
  MKCoordinateSpan span;
  span.latitudeDelta = 0.02;
  span.longitudeDelta = 0.02;
    
  // A MKCoordinateRegion serve para movimentar o 
  // mapa até a região determinada no MKNotation
  MKCoordinateRegion region;
  region.center = coordinate;
  region.span = span;
    
  [self.mapa setRegion:region animated:YES];
  [self.mapa regionThatFits:region]; 
}

#pragma mark - MapView
-(MKAnnotationView *)mapView:(MKMapView *)mapView 
  viewForAnnotation:(id<MKAnnotation>)annotation {
  
  /* Ao adicionar uma notação no mapa, esse método 
     é chamado. É aqui que criamos o pin que aparece
     no mapa */
  MKPinAnnotationView *pin = 
    [[MKPinAnnotationView alloc] initWithAnnotation:
      annotation reuseIdentifier:@"currentLoc"];
    
  pin.pinColor = MKPinAnnotationColorGreen;
  pin.animatesDrop = TRUE;
  pin.canShowCallout = YES;
  pin.calloutOffset = CGPointMake(-5, 5);
    
  return pin;
}

@end
O código está comentado, qualquer dúvida, deixe seus comentários. Agora abra o XIB referente a essa tela e adicione uma SearchBar, um MapView e um SegmentedControl de modo que ele fique conforme a figura abaixo:
No SearchBar, ligue-o com a propriedade searchBar e sete o delegate. Para o MapView, faça o mesmo com a propriedade mapa. E por fim, com o SegmentedControl ligue-o com a propriedade segMapType,  e no evento Value Changed ligue com o método mapViewChanged.

Agora é só rodar e pesquisar pelos lugares desejados.
Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

2 comentários:

Hendi disse...

Ola! Vejo que voce esta usando a nova API do Google Maps para iOS. Eu ja me cadastrei pra receber uma chave faz algum tempo, mas ainda nao obtive resposta. Voce tem alguma dica sobre isso? Tem ideia de que tipo de prioridade o Google esta dando aos pedidos? Obrigado.

Nelson Glauber disse...

Oi Hendi,

A chave é enviada na mesma hora... Você gerou a chave localmente ou no Google Console?

4br4ç05,
nglauber