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

domingo, 16 de setembro de 2012

Paginação com Swipe no Android

Olá povo,

O versão 20 do plugin ADT (Android Development Toolkit) trouxe algumas novidades, entre elas, alguns templates de projetos que podemos optar na criação de um novo projeto Android. Hoje vou  explicar cada um desses templates e ver como utilizar o template que permite fazer uma paginação similar a do Google Play mostrada abaixo.


Note que a tela "Mais rentáveis" está em evidência, mas podemos notar que há uma opção a esquerda e outra a direita na parte superior. Esse é o intuito da paginação, que permitirá ao usuário exibir a próxima informação apenas deslizando para os lados. Vamos mostrar como fazer isso na sua aplicação.
Inicie a criação de um novo projeto e preencha os campos conforme abaixo e pressione Next.


Na segunda tela, há a opção de criarmos uma Activity em branco ou do tipo Mestre/Detalhe. A primeira cria uma Activity simples,  já a segunda, cria uma Activity que exibe uma listagem e os respectivos detalhes. Se a aplicação estiver executando em um tablet, a listagem e os detalhes de um item aparecem na mesma Activity, caso seja um smartphone, a listagem ficará em uma Activity e os detalhes serão exibidos em outra.
Selecione Blank Activity e clique em Next.
A próxima tela do assistente, solicita o nome da Activity que queremos criar, o nome do seu respectivo arquivo de layout e o título da Activity. Mas a informação mais interessante para nós aqui é o campo Navigation Type, que nos dá as seguintes opções:
A opção None não cria nenhuma opção pré-definida de navegação;
Selecionando Tabs, será criada uma estrutura básica de um Activity com abas usando uma ActionBar (que requer Android 4.1);
Tabs + Swipe é igual a anterior, mas será possível alternar entre as abas com o gesto de swipe (também requer Android 4.1);
Swipe Views + Title Strip permite alternar entre views usando swipe, e o título da view corrente é mostrado em um componente PageStrip;
A última opção é a Dropdown, que configura uma ActionBar onde o usuário poderá selecionar, através de um combobox, a tela que deseja abrir. Ao selecionar o item da lista, um Fragment será carregado (também requer Android 4.1)
Vamos utilizar a opção Swipe Views + Title Strip, pois funcionará em todas as versões do Android. Selecione-a e clique em Finish.
Uma coisa curiosa, é que para utilizar os templates acima, o assistente exige que seja selecionada a versão 4.0.1 do Android na primeira tela do assistente. Mas no caso do template que selecionamos, é possível fazer algumas alterações no projeto para que o mesmo funcione em aparelhos com versões anteriores. Tudo isso, graças a API de compatibilidade do Google (que fica na pasta libs do projeto).
1) Clique com o botão direito sobre o projeto e selecione Properties. Na tela que for exibida, selecione Android do lado esquerdo e em seguida, selecione Android 2.3 (API Level 10) e clique em OK.
2) A Activity dará erro, pois estamos importando duas classes que não existem no Android 2.3: android.app.ActionBar e android.app.FragmentTransaction. Remova esses dois imports.
3) Agora vá no AndroidManifest.xml e modifique o minSdkVersion para 10 (Android 2.3).
<uses-sdk
  android:minSdkVersion="10"
  android:targetSdkVersion="15"/>
4) Dará erro agora nas pastas values-v11, values-v14 e menu. Remova as três.
5) E para finalizar, remova o método onCreateOptionsMenu da sua Activity.

Pronto! Agora é só rodar no seu dispositivo 2.3, o resultado ficará como abaixo:
Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

P.S.: Post relacionado: http://nglauber.blogspot.com.br/2011/09/pagescroll.html

quinta-feira, 6 de setembro de 2012

Artigo "C2DM no Android"

Olá povo,

A revista Java Magazine, traz em sua edição de número 107, uma matéria intitulada "Mensagens da nuvem para o Android", escrita por mim e pelo meu colega Felipe Vasconcelos.

Neste artigo, falamos sobre a Cloud To Device Messaging API (C2DM), que na última Google IO mudou para GCM (Google Cloud Messaging). A C2DM permite que um dispositivo Android receba mensagens de uma aplicação servidor, e então, tomar uma ação como obter uma informação na web.

Em diversas aplicações se faz necessário verificar se existem atualizações (de algum tipo de informação) disponíveis no servidor de tempos em tempos. Com o C2DM isso pode ser mudar, uma vez que quando o servidor tiver alguma atualização, ele poderá avisar para os clientes, que então poderão baixá-las.

Espero que vocês gostem.

4br4ç05,
nglauber