terça-feira, 28 de agosto de 2012

iOS: Lendo JSON

Olá povo,

Nesse post vou mostrar como ler JSON utilizando a API nativa do iOS. Como exemplo, vamos obter os principais tópicos do Twitter conhecidos como Top Trends.
Crie um novo projeto no Xcode, e marque para utilizar Storyboards e o ARC (Automatic Reference Counting). No ViewController principal da aplicação, declare um NSMutableArray que armazenará a lista dos tópicos que serão mostrados na UITableView. Declare também um NSMutableData, que armazenará os bytes do arquivo JSON que iremos ler na conexão HTTP.
Note que nossa classe implementa o protocolo NSURLConnectionDelegate, que contém os métodos que são chamados pela conexão HTTP. No nosso caso, estamos utilizando para ler os bytes da conexão ser travar a Thread de UI.

#import <UIKit/UIKit.h>

@interface NGViewController : UITableViewController
  <NSURLConnectionDelegate> {

  NSMutableArray *trends;
  NSMutableData *data;
}
@end
O arquivo de implementação ficará como abaixo:
#import "NGViewController.h"

@implementation NGViewController

- (void)viewDidLoad {
  [super viewDidLoad];

  trends = [NSMutableArray new];
  data = [NSMutableData new];
    
  NSURL *url = [NSURL URLWithString:
    @"https://api.twitter.com/1/trends/23424768.json"];
  NSURLRequest *request = 
    [[NSURLRequest alloc]initWithURL:url];   
  NSURLConnection *conexao = [[NSURLConnection alloc]
    initWithRequest:request delegate:self];
  [conexao start];
}

- (void)viewDidUnload {
  [super viewDidUnload];
  trends = nil;
  data = nil;
}

// Métodos de UITableViewController

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

  UITableViewCell *cell = [tableView
    dequeueReusableCellWithIdentifier:@"Cell"];
    
  cell.textLabel.text = 
    [trends objectAtIndex:indexPath.row];
    
  return cell;
}

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

- (NSInteger)tableView:(UITableView *)tableView
  numberOfRowsInSection:(NSInteger)section {
  return trends.count;
}

// NSURLConnectionDelegate

- (void)connection:(NSURLConnection *)connection
  didReceiveData:(NSData *)pdata {
  [data appendData:pdata];
}


- (void)connectionDidFinishLoading:
  (NSURLConnection *)connection {   
  id jsonObject = [NSJSONSerialization 
    JSONObjectWithData:data 
    options:NSJSONReadingMutableContainers error:nil];
    
  id jsonTrends =  [
    [jsonObject objectAtIndex:0] 
      objectForKey:@"trends"];
        
  for (NSDictionary *trend in jsonTrends) {
    [trends addObject:[trend objectForKey:@"name"]];
  }
    
  [self.tableView reloadData];
}

@end
No método viewDidLoad inicializamos nossos dois atributos e logo em seguida começamos o processo de conexão com o servidor. Notem que a URL termina com 23424768.json, esse número é o woeid do Brasil, se quiser os tópicos mundiais, basta substituir por 1.json. Mas se quiser obter os tópicos de outro país, basta acessar http://developer.yahoo.com/yql/console/ e digitar o comando select * from geo.places where text="Seu Local". Mais informações sobre woeid aqui.
Em seguida criamos um request e enviamos essa solicitação através da conexão. O parâmetro delegate indica que nossa classe será notificada sobre eventos na conexão. O método viewDidUnload liberará os recursos alocados.
Não vou comentar os métodos de UITableViewController, mais detalhes aqui. Na nossa classe implementamos dois métodos do protocolo NSURLConnectionDelegate. O método connection:didReceiveData alimentará o nosso atributo data com os bytes recebidos pela conexão. Já o método connectionDidFinishLoading será chamado quando os dados terminarem de serem lidos. É nele que estamos fazendo a leitura do JSON.
A classe NSJSONSerializarion recebe um objeto NSData e retorna um objeto. Um documento JSON em Objective-C consta basicamente de NSArray ou NSDictionary. Se acessarmos a URL que definimos no primeiro método da nossa classe, vamos visualizar o arquivo JSON e se quisermos vê-lo de uma forma mais amigável, podemos utilizar o site http://jsonviewer.stack.hu/ e colar o texto do arquivo JSON lá. Então será apresentado o documento de uma forma hierárquica. Conforme a figura abaixo:
O elemento com [] é um array, enquanto que o {} é um dicionário (contento chave/valor). Sendo assim, a variável jsonObject contém um array de apenas um elemento. Dentro desse elemento temos um array chamado trends. Esse array está sendo representado pela variável jsonTrends. Em seguida, percorremos esse array (que tem vários dictionaries) e em cada dictionary obtemos o valor da chave name, que contém o nome do tópico mais comentado.
O resultado da aplicação é mostrado abaixo:
Qualquer dúvida, deixem seus comentários,

4br4ç05,
nglauber

sábado, 18 de agosto de 2012

Pipe igual?

Olá povo,

Recebi um email com uma dúvida simples, mas interessante sobre Java, então resolvi posta aqui. O "|" (chama-se pipe ou popularmente "barra-em-pé") representa o operador OR binário. Quando utilizamos || (dois pipes) em uma expressão booleana, se a primeira for verdadeira, ele nem avalia a segunda.
boolean a = true;
int x = 0;
if (a == true || ++x > 0){
   // O valor de x não mudará
}
x continuará zero, pois a segunda expressão não será avaliada.
Mas quando utilizamos o operador em uma atribuição, estamos fazendo uma operação binária de OR.
int a = 2; // Em binário 00000010
int b = 4; // Em binário 00000100

// Vai ativar os bits de "a" (que é 2) em "b"
// Logo, em binário, ficará 00000110 que é igual a 6
b |= a; 
Você pode se perguntar: "em que isso influencia?" ou ainda "onde vou usar isso?". Essa abordagem é muito usada quando vc quer que uma propriedade possa assumir uma combinação de valores. Por exemplo, uma fonte pode ser bold, itálico e/ou sublinhado.
int bold = 1;      //00001
int italic = 2;    //00010
int underline = 4; //00100
Pra deixar uma fonte, itálico e sublinhada, usaríamos:
int fonte = bold | underline; // 00101 = 5
Aí você pode perguntar: qual a diferença entre isso e o + ? É que quando você usa o OR, se o valor já estiver setado, ele não é alterado. Continuando com o exemplo acima:
// ao invés de usar
fonte += bold; 
// igual a 6 (não seria o que queremos)

// usamos
fonte |= bold; 
// como o primeiro bit já está como 1, fonte continua 5
Já para checar se uma das opções da fonte está habilitada, usamos o operador &.
// assumindo que fonte = 5 = 00101
if (fonte & bold != 0){
  // fonte é negrito
}
Fui claro? :)

4br4ç05,
nglauber

sexta-feira, 10 de agosto de 2012

Cuide da sua AsyncTask

Olá povo,

Eu comecei a escrever esse post na Campus Party Recife 2012. Palestrei sobre Android no evento, e apresentei um exemplo que faz uma busca no Twitter e exibe o resultado dessa pesquisa.
Entretanto, durante a palestra não deu para mostrar e resolver alguns problemas básicos que aplicação apresenta. São eles:
1) O Adapter que preenche a lista não foi implementado de forma eficiente;
2) Dar um feedback visual para o usuário enquanto está baixando informações;
3) Avisar ao usuário quando não houver conexão com a internet;
4) Fazer um tratamento caso tenha ocorrido algum problema durante o parse do JSON que é retornado pelo Twitter;
5) Tratar mudança de orientação para evitar que os dados sejam baixados novamente.

O objetivo desse post é mostrar como solucionar esses problemas. Mas antes de começar, vamos mostrar as duas classes básicas da aplicação: Tweet e BuscaTwitter.
public class Tweet {
  String text;
  String profile_image_url;
 
  public String getText() { 
    return text; 
  }
  public void setText(String text) { 
    this.text = text;
  }
  public String getProfile_image_url() {
    return profile_image_url;
  }
  public void setProfile_image_url(String profile_image_url) {
    this.profile_image_url = profile_image_url;
  }
}
public class BuscaTwitter {
  List<Tweet> results;

  public List<Tweet> getResults() {
    return results;
  }
  public void setResults(List<Tweet> result) {
    this.results = result;
  }
}
A primeira mapeia representará cada tweet postado pelo usuário com a hashtag #CPRecife. E a segunda representa a lista desses tweets. Criei essas classes para utilizar a biblioteca GSON que comentarei mais adiante. Vamos agora a lista das soluções.

Solução 1
A classe TweetAdapter utiliza o padrão Adapter para preencher as informações do componente visual de lista. Diferentemente da versão criada na palestra, a versão abaixo é implementada de forma eficiente.
public class TweetAdapter extends ArrayAdapter<Tweet> {

  public TweetAdapter(
    Context context, List<Tweet> objects) {
    super(context, 0, 0, objects);
  }

  public View getView(
    int position, View convertView, ViewGroup parent) {

    ViewHolder holder;

    Tweet tweet = getItem(position);

    if (convertView == null) {
      convertView = LayoutInflater.from(
        getContext()).inflate(
          R.layout.linha_tweet, null);

      holder = new ViewHolder();
      holder.txtTexto = (TextView) 
        convertView.findViewById(R.id.textView1);
      holder.imgFoto = (ImageView) 
        convertView.findViewById(R.id.imageView1);
      convertView.setTag(holder);
    } else {
      holder = (ViewHolder) convertView.getTag();
    }

    holder.txtTexto.setText(tweet.getText());
    BitmapManager.getInstance().loadBitmap(
      tweet.getProfile_image_url(),
      holder.imgFoto);

    return convertView;
  }

  static class ViewHolder {
    ImageView imgFoto;
    TextView txtTexto;
  }
} 
Não entendeu essa classe? Clique aqui.
O arquivo de layout usado pelo adapter é mostrado abaixo:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="horizontal" >

  <ImageView
    android:id="@+id/imageView1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/ic_launcher" />

  <TextView
    android:id="@+id/textView1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Medium Text"
    android:textAppearance="?android:attr/textAppearanceMedium" />
</LinearLayout>
No exemplo acima, estou usando a classe BitmapManager que faz o download de forma assíncrona e por demanda da foto do perfil do usuário que postou o Tweet. Além disso, ela faz um cache em memória dessas imagens. Disponibilizei essa classe aqui, mas se quiser melhora-la, você pode salvar as imagens no cartão de memória do aparelho.

Solucão 2, 3, 4 e 5 :)
As demais soluções são implementadas no Fragment e na AsyncTask. A Activity do projeto é mostrada abaixo.
public class ListTweetActivity 
  extends FragmentActivity {

  @Override
  protected void onCreate(Bundle savedInstance) {
    super.onCreate(savedInstance);
    setContentView(R.layout.activity_list_tweet);
  }
}
A Activity acima, apenas exibe o arquivo de layout abaixo, que contém apenas um fragmento:
<fragment 
  xmlns:android="http://schemas.android.com/apk/res/android"
  class="ngvl.android.cprecife.ListTweetFragment"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:tag="listTweets" />
A maior parte da solução é feita no fragmento e na AsyncTask descritas na classe abaixo. Não conhece fragments? Clique aqui.
public class ListTweetFragment 
  extends ListFragment 
  implements OnClickListener {

  private TweetAsyncTask asyncTask;
  private List<Tweet> tweets; 
  private ProgressDialog dialog;
 
  @Override
  public View onCreateView(LayoutInflater inflater, 
    ViewGroup container, Bundle savedInstanceState) {

    View layout = inflater.inflate(
      R.layout.fragment_list_tweet, container); 
  
    layout.findViewById(R.id.btnRefresh)
      .setOnClickListener(this);
  
    return layout;
  }
 
  @Override
  public void onActivityCreated(
    Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    setRetainInstance(true);
  
    if (asyncTask == null){
      iniciarDownload();
    } else if 
      (asyncTask.getStatus() == Status.RUNNING){
      showDialog();
    }
  }
 
  @Override
  public void onDetach() {
    super.onDetach();
    if (dialog != null) dialog.dismiss();
  }
 
  public void onClick(View v) {
    iniciarDownload();
  }

  private void showDialog() {
    dialog = ProgressDialog.show(getActivity(),
      "Aguarde", "Carregando tweets");
  }

  private void iniciarDownload(){
    ConnectivityManager cm = (ConnectivityManager)
      getActivity().getSystemService(
        Context.CONNECTIVITY_SERVICE);
  
    int wifi = ConnectivityManager.TYPE_WIFI;
    int mobile = ConnectivityManager.TYPE_MOBILE;
  
    if (cm.getNetworkInfo(mobile).isConnected() ||
        cm.getNetworkInfo(wifi).isConnected()){

      asyncTask = new TweetAsyncTask();
      asyncTask.execute();

    } else {
      Toast.makeText(getActivity(), 
        "Sem conexão com a internet", 
          Toast.LENGTH_SHORT).show();
    }
  }
 
  private void configuraAdapter(List<Tweet> tweets){
    TweetAdapter adapter = 
      new TweetAdapter(getActivity(), tweets);
    setListAdapter(adapter);
  }
 
  class TweetAsyncTask extends 
    AsyncTask<Void, Void, BuscaTwitter> {
  
    @Override
    protected void onPreExecute() {
      super.onPreExecute();
      showDialog();
    }
  
    @Override
    protected BuscaTwitter doInBackground(
      Void... params) {

      String url = 
        "http://search.twitter.com/"+
        "search.json?q=CPRecife";
      try {
        InputStream is = new URL(url).openStream();
        Gson gson = new Gson();
        BuscaTwitter resultadoBusca = gson.fromJson(
          new InputStreamReader(is), 
          BuscaTwitter.class);
        return resultadoBusca;

      } catch (Exception e) {
        e.printStackTrace();
      }
      return null;
    }

    @Override
    protected void onPostExecute(BuscaTwitter result) {
      super.onPostExecute(result);
      if (result != null && 
          result.getResults() != null) {
        tweets = result.getResults();
        configuraAdapter(tweets);
    
      } else {
        Toast.makeText(getActivity(), 
          "Falha ao carregar tweets", 
          Toast.LENGTH_SHORT).show();
      }
      dialog.dismiss();
    }
  }
}
Solução 2
Uma solução para dar um feedback para o usuário é a ProgressDialog. Ele será exibido no método onPreExecute, e retirado da tela no método onPostExecute. Esses métodos são chamados, como o próprio nome diz, antes e depois do download das informações. Se você não conhece a AsyncTask, da uma olhada aqui.

Solução 3
Para verificar se existe conexão com a internet, estamos utilizando a classe ConnectivityManager. Essa checagem está sendo feita no método iniciarDownload. Para utilizar essa classe é necessário adicionar a permissão ACCESS_NETWORK_STATE. Mais informações, é só olhar esse post aqui.

Solução 4
Essa é a mais simples. No método que está baixando o JSON (doInBackground) temos um try/catch, para que caso ocorra algum problema, a exceção seja capturada. Nesse caso o método retornará null. Assim, se no método onPostExecute, o parâmetro result vier nulo é porque houve algum problema. E nesse caso, mostrar uma mensagem pro usuário.

Solução 5
Essa é a parte mais complexa do post. Ao girar o aparelho, (por padrão) o Android destrói e recria a Activity que está sendo exibida. Para tratar esse comportamento, temos 3 opções conforme falei nesse post aqui. As duas primeiras (fixar uma orientação e evitar que a Activity seja recriada) não são boas opções de design de software, pois elas impedem que criemos layouts diferentes para as duas orientações, e também impedem que ao mudarmos de idioma, a Activity recarregue os textos do idioma corrente.
Sendo assim, a API de Fragmentos nos dá a opção de manter seu estado mesmo que a activity seja destruída. Fazemo isso com o método setRetainInstance do Fragment. No nosso fragmento temos três atributos:
  • a AsyncTask que ferá o download das informações;
  • uma lista com os Tweets que já foram baixados, e vai servir para que não tenhamos que baixar a informação novamente quando girarmos o aparelho; 
  • e um ProgressDialog que falamos na solução 2.
No método onActivityCreate verificamos se a AsyncTask é igual a nulo, neste caso iniciamos o download das informações. Caso contrário, verificamos se a AsyncTask ainda está executando, em caso positivo, apenas exibimos o ProgressDialog.

Ao clicar no botão refresh, não podemos solicitar que a AsyncTask execute novamente, pois semelhante a classe Thread, não podemos reusá-la. Nesse caso, precisamos criar uma nova.

Para finalizar, se vocês observarem, estou utilizando a classe Gson para ler o JSON retornado pelo Twitter. Essa classe faz parte da biblioteca GSON, e faz o parse automático de um JSON para um objeto Java, desde que eles tenham a mestra estrutura. No nosso exemplo, a estrutura é representada pelas classes BuscaTwitter e Tweet. Para baixar a biblioteca do GSON é só clicar aqui. Descompacte o arquivo e coloque o JAR na pasta libs do projeto.

O arquivo de layout do fragment é mostrado abaixo:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:id="@+id/LinearLayout1"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical" >

  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#cccccc" >
    <TextView
      android:id="@+id/textView1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_gravity="center_vertical"
      android:layout_weight="1"
      android:text="#CPRecife"
      android:textAppearance="?android:attr/textAppearanceLarge" />
    <ImageButton
      android:id="@+id/btnRefresh"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:src="@android:drawable/stat_notify_sync" />
  </LinearLayout>

  <ListView
    android:id="@android:id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
</LinearLayout>

Pronto! Agora você pode executar a aplicação e girar o aparelho a vontade que tudo deve funcionar bem :)

É isso pessoal. Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

sábado, 14 de julho de 2012

Palestra de Android na Campus Party Recife

Olá povo,

Tive o imenso orgulho de ser convidado pelo Prof. Silvio Meira para ministrar uma palestra sobre Android na Campus Party Recife. O título da apresentação será "Sua primeira e próximas aplicações Android: como fazer em uma hora?" e acontecerá na sexta, 27/07/12 às 11:15 no cenário Pitágoras.

Na ocasião, mostrarei os principais recursos utilizados para criar aplicativos para a plataforma Android. A abordagem será bem prática, mostrando as ferramentas necessárias, estrutura de um projeto,  ferramentas que facilitam a vida dos desenvolvedores e que fazem da plataforma Google líder no mercado mobile. Abrirei espaço para que os presentes possam tirar dúvidas em relação a plataforma de uma maneira geral.

Falando um pouquinho sobre a Campus Party, ela foi criada na Espanha em 1997 e é atualmente o maior evento de tecnologia, inovação, ciência, entretenimento e cultura digital do mundo. Ela funciona de forma itinerante, onde os participantes mudam-se com seus computadores, malas e barracas para dentro de uma arena, onde frequentam oficinas, palestras, conferências, competições e atividades de lazer. Em 2008, aconteceu a primeira edição brasileira, que desde então acontecia apenas em São Paulo. Entre os dias 26 e 30 de julho de 2012, Recife receberá pela primeira vez o evento. 

A #CPRecife será divida em cenários onde acontecerão palestras, oficinas, mesas de discussão, entre outras. O cenário Pitágoras será o local onde os "Campuseiros Desenvolvendo" poderão discutir/conhecer tópicos relacionados com desenvolvimento de software, software livre e segurança. Lá também estarão meus colegas Eric Cavalcanti, falando sobre programação mobile multiplataforma com Titanium, e Richardson Oliveira falando sobre desenvolvimento para a plataforma iOS.

Os ingressos para o evento já acabaram. Então, quem já comprou, nos vemos lá!

EDITADO em 27/07/2012

Vocês podem conferir como foi a palestra no vídeo abaixo:

Os slides da palestra estão disponíveis em aqui.

A aplicação feita durante a apresentação está aqui. Ela está tal como fei feita na apresentação. Aproveito pra deixar algumas sugestões de melhoria: tratar a ausência de conexão com a internet; evitar que os dados sejam recarregados ao girar o aparelho.

4br4ç05,
nglauber

quinta-feira, 12 de julho de 2012

Persistência no iOS com Core Data

Olá povo,

Eu já mostrei aqui no blog como persistir informações no iOS utilizando o SQLite. Para quem quer ter um  controle maior sobre o que está sendo salvo no banco de dados, é uma boa opção. Entretanto a Core Data realiza o mapeamento objeto-relacional de uma forma bem interessante e nos poupa de escrever um monte de código de baixo nível (uma vez que a lib do SQLite é escrita em C).

Crie um novo projeto no Xcode do tipo Single View Application e mãos à obra! Se você selecionar o template Master/Detail application no assistente, aparecerá a opção para utilizar Core Data. Isso servirá para incluir a lib do Core Data no projeto e inserir algum código para nós. Se não usar esse template, teremos que adicionar manualmente. Para isso, basta selecionar o projeto, e na aba Build Fases na seção Link Binary With Libraries e clicar em "+". Na lista que será exibida, selecione CoreData.framework.

Vamos agora adicionar ao projeto o arquivo onde ficarão as definições das entidades que serão persistidas no banco. No Xcode, selecione File | New | File... selecione a opção Core Data no lado esquerdo, e então selecione a opção Data Model. Clique em Next e nomeie o arquivo para CarroModel, em seguida,  clique em Create para concluir o assistente.

Selecione o arquivo recém criado, e clique no botão Add Entity que fica na parte inferior. Renomeie a entidade para Carro. Em seguida, clique no botão Add Attribute, selecione o atributo e o renomeie para "nome" e modifique seu tipo (na janela da direita) para String (através da propriedade Attribute Type). Adicione mais dois atributos, placa e ano, e modifique os seus tipos para String e Int16 respectivamente.
Abaixo a imagem do model após nossas alterações.
Vamos agora criar a classe carro que será persistida no banco de dados. Selecione a entidade Carro e clique no menu File | New | File... Na janela que for exibida, selecione Core Data no lado esquerdo, e então selecione NSManagedObject subclass. Será criada a classe Carro conforme abaixo.
// Carro.h -----------------------------
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>


@interface Carro : NSManagedObject

@property (nonatomic, retain) NSString * nome;
@property (nonatomic, retain) NSString * placa;
@property (nonatomic, retain) NSNumber * ano;

@end

// Carro.m -----------------------------
#import "Carro.h"

@implementation Carro

@dynamic nome;
@dynamic placa;
@dynamic ano;

@end
Notem que nossa classe herda de NSManagedObject o que quer dizer que os objetos dessa classe serão persistidos. Outro detalhe é que os atributos são implementados com @dynamic.

Crie uma nova classe chamada RepositorioCarro e deixe-a conforme abaixo.

#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>

@class Carro;

@interface RepositorioCarro : NSObject {
  NSManagedObjectModel *mom;
  NSPersistentStoreCoordinator *coordinator;
  NSManagedObjectContext *context;
}

- (void) test;
- (Carro *) newCarro;
- (void) salvar;
- (void) excluir:(Carro *)carro;
- (NSArray *) todosCarros;

@end
Nessa classe, declaramos um NSManagedObjectModel, NSPersistentStoreCoordinator e um NSManagedObjectContext. Vamos entender o papel de cada um:
- NSManagedObjectContext é a classe que vai realizar as operações com o banco. Logo, para inserir, alterar, excluir ou obter objetos (NSManagedObject) do banco, utilizamos métodos dessa classe.
- NSPersistentStoreCoordinator é como se fosse a conexão com o banco. É nela onde você define o local onde está armazenado o arquivo do banco e outras configurações adicionais.
- NSManagedObjectModel é o esquema do banco de dados. Que aqui no nosso projeto é o arquivo onde definimos o modelo da classe Carro.

O código abaixo inicia com o "construtor" da classe, que chama o método initContext. Esse método por sua vez chama o initCoordinator que por sua vez chama o initMom.

#import "RepositorioCarro.h"
#import "Carro.h"

@implementation RepositorioCarro

- (id) init {
  self = [super init];  
  [self initContext];
  return self;
}

// Retorna a URL para o diretório Documents
- (NSURL *)appDocsDir {
  return [[[NSFileManager defaultManager] 
    URLsForDirectory:NSDocumentDirectory 
      inDomains:NSUserDomainMask] lastObject];
}

// Carrega o arquivo de modelo
- (void) initMom {
  NSURL *modelURL = [[NSBundle mainBundle] 
    URLForResource:@"CarroModel" 
      withExtension:@"momd"];

  mom = [[NSManagedObjectModel alloc] 
    initWithContentsOfURL:modelURL];
}

// Abre a conexão com o banco
- (void) initCoordinator {
  [self initMom];    
  NSURL *storeURL = [[self appDocsDir] 
    URLByAppendingPathComponent:
     @"CarrosDB.sqlite"];
        
  NSError *error = nil;
  coordinator =
    [[NSPersistentStoreCoordinator alloc]
      initWithManagedObjectModel:mom];

  if (![coordinator 
    addPersistentStoreWithType:NSSQLiteStoreType
    configuration:nil URL:storeURL
    options:nil error:&error]) {
    NSLog(@"Erro... %@",
      [error localizedDescription]);
  }
}

// Inicializa o contexto
- (void) initContext {
  [self initCoordinator];
    
  context = [[NSManagedObjectContext alloc] init];
  [context setPersistentStoreCoordinator:coordinator];
}

- (void) test {
  // Testando inserção
  Carro *carro = [self newCarro];
    
  carro.nome  = @"Uno";
  carro.placa = @"UNO0001";
  carro.ano   = [NSNumber numberWithInt:2000];
    
  [self commit];

  // Testando alteração
  carro = @"Palio";
  [self commit];
  // Testando listagem...
  NSArray *fetchedObjects = [self todosCarros];
  for (Carro *carro in fetchedObjects) {
    NSLog(@"Nome: %@", carro.nome);
    NSLog(@"placa: %@", carro.placa);
  }
}

// A classe carro não deve ter construtor e 
// deve ser inicializada dessa forma.
- (Carro *) newCarro {
  return [NSEntityDescription
    insertNewObjectForEntityForName:@"Carro"
    inManagedObjectContext:context];

}

// As alterações que são feitas nos objetos
// são persistidas no banco ao dar commit
- (void) commit {
    NSError *error;
    if (![context save:&error]) {
      NSLog(@"Erro... %@", 
        [error localizedDescription]);
    }
}

- (void) excluir:(Carro *)carro {
    [context deleteObject:carro];  
}

- (NSArray *) todosCarros {
  NSFetchRequest *fetchRequest = 
    [[NSFetchRequest alloc] init];

  NSEntityDescription *entity = 
    [NSEntityDescription entityForName:@"Carro"
       inManagedObjectContext:context];

  [fetchRequest setEntity:entity];

  NSError *error;
  return [context 
    executeFetchRequest:fetchRequest 
      error:&error];
}

@end

O método newCarro cria uma nova instância de um objeto carro no contexto. Ou seja, se comitarmos esse objeto ele já estará persistido no banco. Se alterarmos uma propriedade de um objeto carro do array que é retornado pelo método todosCarros, e depois chamarmos o método commit, a alteração já é realizada no banco de dados.
O método test, como próprio nome diz, serve para testarmos nossa classe. Onde inserimos, alteramos e listamos as informações do banco.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

Fonte: http://www.raywenderlich.com/934/core-data-on-ios-5-tutorial-getting-started

sábado, 30 de junho de 2012

iOS: Dicas

Olá povo,

Como tô mexendo muito com iOS ultimamente vou registrar aqui algumas pequenas dicas no desenvolvimento de aplicações para iOS.

Dica 1 - Adicionando botões na navigation bar dinâmicamente
UIBarButtonItem *saveButton = [[UIBarButtonItem alloc]
  initWithTitle:@"Salvar" 
  style:UIBarButtonItemStyleDone 
  target:self action:@selector(salvarDados)];
[self.navigationItem setRightBarButtonItem:saveButton];

Dica 2 - Ocultando o teclado virtual

O teclado virtual do iOS é exibido automaticamente quando você clica em uma caixa de texto (UITextFiedl), se tornando o "primeiro a responder" (firstResponder) a eventos de toque. Entretanto para fechá-la você vai codificar um pouquinho. Digamos que em uma tela você tenha 3 caixas de texto. Quando o usuário estiver na primeira caixa de texto ele terá a opção de pular para o próximo campo clicando no botão Next no teclado virtual. Para habilitar esse botão altere a propriedade Return Button do TextField para Next. Faça o mesmo no segundo (e nos outros que você precisar). No último coloque Done na mesma propriedade.
Agora ligue o evento Did End On Exit de todas as caixas de texto para o método abaixo.

- (IBAction)hideKeyboard:(id)sender {
  if ([sender isEqual:edtName] == YES){
    [edtAddress becomeFirstResponder];
  } else if ([sender isEqual:edtAddress] == YES){
    [edtPhone becomeFirstResponder];
  } else {
    [edtName resignFirstResponder];
    [edtAddress resignFirstResponder];
    [edtPhone resignFirstResponder];
  }
}
Isso resolve o problema da navegação. Mas uma coisa comum é, ao clicarmos fora da caixa de texto, o teclado desaparecer. Para fazer isso, clique na View da tela e mude seu tipo de classe de UIView para UIControl. Depois associe o evento Touch Up Inside para o método acima.
O parâmetro sender identifica qual componente disparou o evento. Se não foi nenhum dos dois primeiros TextFields ocultamos o teclado chamando o método resignFirstResponder (o TextField agora é o "primeiro respondedor"). Pronto! Agora ao clicar em qualquer área da tela, o teclado desaparecerá.

Dica 3 - Editando células da UITableView

Quando precisamos, em algum tipo de listagem, excluir um registro, podemos recorrer a um recurso bacana do iOS: deslizar sobre célula. Quando isso acontece o método tableView:editingStyleForRowAtIndexPath é chamado. Aqui podemos retornar um estilo de edição para a célula: UITableViewCellEditingStyleDelete ou UITableViewCellEditingStyleInsert.
-(UITableViewCellEditingStyle)tableView:
    (UITableView *)tableView 
  editingStyleForRowAtIndexPath:
    (NSIndexPath *)indexPath{

    return UITableViewCellEditingStyleDelete;
}
Quando deslizar o dedo para a direita sobre a célula, aparecerá ao botão Delete na célula. Para tratar o evento desse botão devemos implementa o método abaixo:
-(void)tableView:(UITableView *)tableView 
 didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath {
}
Uma outra opção é colocar utilizar o código abaixo ao clicar em um UIBarButton.
-(void)habilitarExcluir:(id)sender {
  [self.tableView setEditing:
    !self.tableView.editing animated:YES];
}
E realizar o código da exclusão no método abaixo:
- (void)tableView:(UITableView *)tableView 
  commitEditingStyle:(UITableViewCellEditingStyle)editingStyle 
  forRowAtIndexPath:(NSIndexPath *)indexPath {

  if (editingStyle == 
    UITableViewCellEditingStyleDelete) {
  }   
}

Dica 4 - Abrindo uma URL no Browser

O código abaixo também aplica-se a telefone (tel:88990099) e mapas...
NSURL *url = [NSURL URLWithString:
  @"http://nglauber.blogspot.com"];
[[UIApplication sharedApplication] openURL:url];

Dica 5 - iPhone ou iPad?

Em uma aplicação Universal (que roda em iPhone e iPad) para checar se o device é um iPhone (ou iPod) ou iPad utilizamos o código abaixo:
if ([[UIDevice currentDevice] userInterfaceIdiom] == 
  UIUserInterfaceIdiomPhone) {
  //iPhone ou iPod
} else {
  // iPad
}

Dica 6 - Salvando Objetos com NSUserDefaults

Para salvar um objeto todo no NSUserDefaults você deve implementar os métodos que vão serializar e deserializar os dados...
// NSPessoa.h --------------------------------
#import 

@interface NSPessoa : NSObject

@property (nonatomic, strong) NSString *nome;
@property (nonatomic, strong) NSString *email;

@end

// NSPessoa.m --------------------------------
#import "NSPessoa.h"

@implementation NSPessoa

@synthesize nome, email;

- (void)encodeWithCoder:(NSCoder *)encoder {
  [encoder encodeObject:self.nome  forKey:@"nome"];
  [encoder encodeObject:self.email forKey:@"email"];
}

- (id)initWithCoder:(NSCoder *)decoder {
  if((self = [super init])) {
    self.nome  = [decoder decodeObjectForKey:@"nome"];
    self.email = [decoder decodeObjectForKey:@"email"];
  }
  return self;
}
@end

Abaixo o código para carregar e salvar os dados.
// Carregando ----------------------------------
NSUserDefaults *prefs = 
  [NSUserDefaults standardUserDefaults];

NSData *data = [prefs objectForKey:@"pessoa"];
NSPessoa *pessoa = (NSPessoa *)
  [NSKeyedUnarchiver unarchiveObjectWithData: data];

// se os dados da pessoa não existirem, 
// inicia um novo objeto    
if (!pessoa){
  pessoa = [NSPessoa new];
}

// Salvando ------------------------------------
NSData *data = [NSKeyedArchiver 
  archivedDataWithRootObject:pessoa];

NSUserDefaults *defaults = 
  [NSUserDefaults standardUserDefaults];

[defaults setObject:data forKey:@"pessoa"];

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

segunda-feira, 25 de junho de 2012

SQLite no iOS

Olá povo,

Nesse post vou mostrar como utilizar o SQLite em aplicações iOS através da criação de um cadastro simples. Vou utilizar também o recurso de Storyboards (veja esse post) para agilizar a criação das telas.
Comece criando um projeto no Xcode do tipo "Single View Application" que vou nomeá-lo de ExemploSQLiteBlog. Lembre-se de deixar habilitadas as opções Use storyboards e Use automatic reference counting. Após criar o projeto, devemos adicionar a biblioteca do SQLite ao nosso projeto. Para tal, clique sobre o projeto, depois selecione a aba Build phases. Na seção Link Binary With Libraries clique no botão "+" e selecione libsqlite3.dylib.
Uma vez que o projeto está configurado, vamos fazer toda a parte de "negócio" da aplicação, onde criaremos a classe de dados da aplicação, assim como a classe que fará a persistência dos dados da aplicação.
Crie uma nova classe chamada NGContact conforme abaixo (a utilização de prefixos no nome da classe em Objective-C é comum para evitar duplicidade):
// NGContact.h ----------------------
#import <Foundation/Foundation.h>

@interface NGContact : NSObject

@property (assign) NSInteger _id;
@property (strong, nonatomic) NSString *name;
@property (strong, nonatomic) NSString *address;
@property (strong, nonatomic) NSString *phone;

- (id)initWithName:(NSString *)name 
  andAddress:(NSString *)address 
  andPhone:(NSString *)phone;

@end

// NGContact.m ----------------------
#import "NGContact.h"

@implementation NGContact

@synthesize _id = __id, name = _name, 
  address = _address, phone = _phone;

- (id)initWithName:(NSString *)name 
  andAddress:(NSString *)address 
  andPhone:(NSString *)phone {

  self = [super init];
  if (self){
    self.name = name;
    self.phone = phone;
    self.address = address;
  }
  return self;
}

@end
Aqui temos uma classe simples com 4 propriedades (id, nome, endereço e telefone) e um método para inicializar esses atributos. Para quem é "do Java" (como eu) esse método servirá como um construtor (lembrando que Objective-C não tem construtores).
Vamos agora criar a classe que fará a persistência desses objetos e utilizará a API do SQLite. Ela servirá de repositório da aplicação e a chamaremos de NGContactDB, e o arquivo de cabeçalho (.h) deve ficar conforme abaixo.
// NGContactDB.h
#import <Foundation/Foundation.h>
#import <sqlite3.h>

@class NGContact;

@interface NGContactDB : NSObject {
  sqlite3 *contactDB;
  NSString *databasePath;
}

- (void)createDatabase;
- (void)insertContact:(NGContact *)contact;
- (void)updateContact:(NGContact *)contact;
- (void)deleteContact:(NGContact *)contact;
- (NSArray *)getAllContacts;

@end
Como podemos observar, essa classe contém dois atributos, o primeiro é uma referência para sqlite3, e o segundo é o caminho do arquivo do banco. Além disso, temos também cinco métodos:
- createDatabase, que como o próprio nome diz, criará o banco caso ele não exista;
- insertContact, updateContact e deleteContact irão respectivamente inserir, atualizar e excluir um contato no banco;
- e getAllContacts irá obter todos os contatos cadastrados no banco.
Deixe o arquivo de implementação conforme abaixo:
#import "NGContactDB.h"
#import "NGContact.h"
#import <sqlite3.h>

@implementation NGContactDB

// Inicializa o banco -------------------------------
- (void)initDB {   
  // Obtém os diretório de arquivos da aplicação
  NSArray *dirPaths = 
    NSSearchPathForDirectoriesInDomains(
      NSDocumentDirectory, NSUserDomainMask, YES);
  // Obtém o diretório para salvar o arquivo do banco
  NSString *docsDir = [dirPaths objectAtIndex:0];
  // Cria o caminho do arquivo do banco
  databasePath = [[NSString alloc] initWithString: 
    [docsDir stringByAppendingPathComponent: 
      @"contacts.db"]];
  
  const char *dbpath = [databasePath UTF8String];
  // Inicializa o atributo contactDB com o banco  
  if (sqlite3_open(dbpath, &contactDB) != SQLITE_OK){
    NSLog(@"Failed to open/create database - 1");
  }
}

// Cria a tabela no banco ---------------------------
- (void)createDatabase {   
  // Chama o método acima...
  [self initDB];
    
  char *errMsg;
  const char *sql_stmt = "CREATE TABLE IF NOT EXISTS 
    CONTACTS (ID INTEGER PRIMARY KEY AUTOINCREMENT, 
    NAME TEXT, ADDRESS TEXT, PHONE TEXT)";

  // Cria a tabela no banco se ela não existir          
  if (sqlite3_exec(contactDB, sql_stmt, NULL, NULL, 
    &errMsg) == SQLITE_OK) {
    NSLog(@"Database successfully created");
  } else {
    NSLog(@"Failed to create table");
  }
            
  sqlite3_close(contactDB);
}

// Inserir um contato no banco ---------------------
- (void)insertContact:(NGContact *)contact {
  [self initDB];
      
  char *sql = "INSERT INTO CONTACTS (NAME,ADDRESS, 
    PHONE) VALUES (?, ?, ?);";

  sqlite3_stmt *stmt;
  if (sqlite3_prepare_v2(contactDB, sql, -1, 
    &stmt, nil) == SQLITE_OK) {

    sqlite3_bind_text(stmt, 1, 
      [contact.name UTF8String], -1, NULL);
    sqlite3_bind_text(stmt, 2, 
      [contact.address UTF8String], -1, NULL); 
    sqlite3_bind_text(stmt, 3, 
      [contact.phone UTF8String], -1, NULL); 
  }
  if (sqlite3_step(stmt) == SQLITE_DONE){
    NSLog(@"Record added");
  } else {
    NSLog(@"Failed to add contact");
  }
  sqlite3_finalize(stmt);
  sqlite3_close(contactDB);
}

// Atualizar contato no banco ----------------------
- (void)updateContact:(NGContact *)contact {
  [self initDB];
      
  char *sql = "UPDATE CONTACTS SET NAME = ?, 
    ADDRESS = ?, PHONE = ? WHERE ID = ?;";

  sqlite3_stmt *stmt;
  if (sqlite3_prepare_v2(contactDB, sql, -1, 
    &stmt, nil) == SQLITE_OK) {
    sqlite3_bind_text(stmt, 1, 
      [contact.name UTF8String], -1, NULL);
    sqlite3_bind_text(stmt, 2,
      [contact.address UTF8String], -1, NULL); 
    sqlite3_bind_text(stmt, 3, 
      [contact.phone UTF8String], -1, NULL); 
    sqlite3_bind_int(stmt, 4, contact._id);
  }

  if (sqlite3_step(stmt) == SQLITE_DONE){
    NSLog(@"Record updated");
  } else {
    NSLog(@"Failed to update contact");
  }
  sqlite3_finalize(stmt);
  sqlite3_close(contactDB);
}

// Excluir contato ---------------------------------
- (void)deleteContact:(NGContact *)contact {
  [self initDB];
      
  char *sql = "DELETE FROM CONTACTS WHERE ID = ?;";
  sqlite3_stmt *stmt;
  if (sqlite3_prepare_v2(contactDB, sql, -1, 
    &stmt, nil) == SQLITE_OK) {

    sqlite3_bind_int(stmt, 1, contact._id); 
  }
  if (sqlite3_step(stmt) == SQLITE_DONE){
    NSLog(@"Record removed");
  } else {
    NSLog(@"Failed to removed contact");
  }
  sqlite3_finalize(stmt);
  sqlite3_close(contactDB);
}

// Obter lista de objetos do banco -----------------
- (NSArray *)getAllContacts;
{
  [self initDB];
    
  NSMutableArray *contacts = 
    [[NSMutableArray alloc]init];
    
  sqlite3_stmt *statement;
    
  NSString *querySQL = [NSString stringWithFormat: 
    @"SELECT * FROM contacts"];
        
  const char *query_stmt = [querySQL UTF8String];
        
  if (sqlite3_prepare_v2(contactDB, query_stmt, -1, 
    &statement, NULL) == SQLITE_OK){

    while (sqlite3_step(statement) == SQLITE_ROW)
    {
      NSInteger contactId = 
        sqlite3_column_int(statement, 0);
               
      NSString *nameField = [[NSString alloc] 
        initWithUTF8String:(const char *) 
          sqlite3_column_text(statement, 1)];
                
      NSString *addressField = [[NSString alloc] 
        initWithUTF8String:(const char *) 
          sqlite3_column_text(statement, 2)];
                
      NSString *phoneField = [[NSString alloc] 
        initWithUTF8String:(const char *) 
          sqlite3_column_text(statement, 3)];
                
      NGContact *contact = [[NGContact alloc]init];
      contact._id = contactId;
      contact.name = nameField;
      contact.address = addressField;
      contact.phone = phoneField;
                
      [contacts addObject:contact];
    }
    sqlite3_finalize(statement);
  }
  sqlite3_close(contactDB);
    
  return [NSArray arrayWithArray:contacts];
}

@end
O código está comentado, mas vou fazer algumas observações. O método initDB começa tentando obter o caminho do diretório de documentos, na qual a aplicação pode salvar seus arquivos. Em seguida monta o caminho do arquivo de banco de dados com a variável databasePath. Por fim, utilizamos a função sqlite3_open (isso mesmo, função, já que o sqlite é escrito em C) para abrir o arquivo de banco de dados e inicializar o atributo contactDB.
Já o método createDatabase criará a tabela Contatcts no banco de dados caso ela não exista. Esse método deve ser chamado apenas uma vez durante o ciclo de vida da aplicação. Pois, uma vez criada, a tabela permanecerá no banco. Esse método começa chamado o initDB para inicializar o atributo contactDB depois monta o SQL para criação da tabela. Note que esse SQL é montado com um char*, que é o tipo esperado pela biblioteca (e não NSString).  Para executar o comando no banco, utilizamos a função sqlite3_exec, passando a referência para o banco e a sentença SQL (para os demais parâmetros olhe aqui).
Os métodos insertContact, updateContact e deleteContact são bem similares. Chamamos o método initDB para inicializar o atributo contactDB. Depois declaramos um sqlite3_stmt que é inicializado através da função sqlite3_prepare_v2 que recebe a referência do banco, a instrução SQL e a referência para o statement. Cada uma das "?" será um parâmetro que será preenchida com um valor através da função sqlite3_bind_* (onde * é o tipo do parâmetro que se quer passar). Para executar a instrução SQL, utilizamos a função sqlite3_step, se tudo correr bem, será retornado SQLITE_DONE. Por fim, fechamos o statement e o banco com as funções sqlite3_finalizesqlite3_close respectivamente.
O método que retorna uma lista de objetos NGContact utiliza os mesmos conceitos dos métodos anteriores, a diferença é que para percorrer os registros retornados, a função sqlite3_step retorna SQLITE_ROW quando temos uma linha para ler. E para ler essa coluna, utilizamos as funções sqlite3_column_* (onde * é o tipo da coluna).

Bem pessoal. Da parte de SQLite é isso, em um próximo post vou mostrar como integrar essa classe com uma UITableViewController (para mostrar os dados) e criar uma tela para inserir e alterar os registros. Mas quem quiser ir se adiantando, pode usar esse post aqui.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber