curso de desenvolvimento de aplicações para ios com objective-c
DESCRIPTION
Primeira versão do material do curso de desenvolvimento de aplicações para iOS usando Objective-C.TRANSCRIPT
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
1
Desenvolvendo aplicações para iOS usando Objective-‐C
Maurício Linhares [email protected]
DESENVOLVIMENTO DE APLICAÇÕES PARA IOS USANDO OBJECTIVE-‐C 1 INTRODUÇÃO 2 CAPÍTULO 1 -‐ CONHECENDO OBJECTIVE-‐C 3 IMAGEM 1 – SELEÇÃO DE PROJETOS NO XCODE 3 IMAGEM 2 – CRIANDO O PROJETO NO XCODE 4 IMAGEM 3 – VISUALIZAÇÃO INICIAL DO PROJETO 5 IMAGEM 4 – CRIANDO O TARGET QUE VAI RODAR OS NOSSOS TESTES 6 IMAGEM 5 – ADICIONANDO O EXECUTÁVEL DA APLICAÇÃO COMO DEPENDÊNCIA AO TARGET TEST 7 IMAGEM 6 – CRIANDO A NOSSA PRIMEIRA CLASSE EM OBJECTIVE-‐C 8 IMAGEM 7 – CRIANDO A NOSSA PRIMEIRA CLASSE EM OBJECTIVE-‐C 9 IMAGEM 8 – VISUALIZAÇÃO DO NAVEGADOR DE ARQUIVOS COM A CLASSE CONTA CRIADA. 9 LISTAGEM 1 – CONTA.H 9 LISTAGEM 2 – CONTA.M 11 LISTAGEM 3 – CONTA.H 11 LISTAGEM 4 – CONTA.M 12 CRIANDO O NOSSO PRIMEIRO TESTE UNITÁRIO 12 IMAGEM 9 – CRIANDO O GRUPO “TEST” 13 IMAGEM 10 – CRIANDO A CLASSE DE TESTES 13 IMAGEM 11 – SELECIONANDO OS TARGETS DA CLASSE DE TESTES 14 IMPLEMENTANDO O NOSSO PRIMEIRO TESTE 14 LISTAGEM 5 – CONTATEST.M 14 IMAGEM 12 – ALTERANDO O TARGET PADRÃO PARA TEST 16 IMAGEM 13 – DRAG E DROP DO ARQUIVO “CONTA.M” EM TEST 16 IMAGEM 14 – INDICAÇÃO DE ERROS DE BUILD DO XCODE 17 IMAGEM 15 – TELA DE ERROS NO BUILD DO XCODE 18 LISTAGEM 6 – CONTA.H 18 LISTAGEM 7 – CONTA.M 19 LISTAGEM 8 – CONTATEST.H 20 LISTAGEM 9 – CONTATEST.M 20 CRIANDO UM “CONSTRUTOR” PARA O NOSSO OBJETO CONTA 22 LISTAGEM 10 – CONTA.H – DECLARANDO O MÉTODO INITWITHSALDO 22 LISTAGEM 11 – CONTA.M 23 DEFININDO PROPRIEDADES AUTOMATICAMENTE NOS OBJETOS 23 LISTAGEM 12 – CONTA.H COM AS NOVAS PROPRIEDADES DEFINIDAS 24 LISTAGEM 13 – CONTA.M COM A DEFINIÇÃO DAS PROPRIEDADES E GERENCIAMENTO DE MEMÓRIA 25 LISTAGEM 14 – CONTATEST.M COM O CÓDIGO DE GERENCIAMENTO DE MEMÓRIA 26
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
2
Introdução
Este material tem como objetivo servir de referência para o curso de desenvolvimento de aplicações usando Objective-‐C e XCode para iOS, o sistema operacional para dispositivos móveis da Apple, como iPhones, iPods e iPads. Ele faz parte do material complementar para as aulas expositivas do curso de desenvolvimento para iOS.
Para seguir esse material você precisa de um computador com MacOS e XCode instalados, além do SDK para desenvolvimento de aplicações para iOS. O material também assume que você já tem experiência com o desenvolvimento de software em ao menos uma linguagem orientada a objetos.
Conceitos básicos de programação orientada a objetos como variáveis de instância, métodos, construtores, herança, encapsulamento não vão ser explicados, assume-‐se que quem está lendo o material já tem conhecimento de todos esses conceitos que são lugar comum em qualquer linguagem de programação orientada a objetos.
Em várias partes do material você vai encontrar a fala “Abra o menu contextual do item X” ou “clique com o botão direito em X”, isso quer dizer usar o botão direito do mouse (em um mouse comom) fazer “Control + Click” no item selecionado ou, se você estiver usando um trackpad multi-‐touch, clicar com os dois dedos ao mesmo tempo.
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
3
Capítulo 1 -‐ Conhecendo Objective-‐C
O código fonte deste primeiro capítulo está disponível em -‐ https://github.com/mauricio/capitulo-‐1-‐curso-‐ios Objective-‐C é uma linguagem de programação orientada a objetos de uso geral e é a língua padrão para o desenvolvimento de aplicações para o Mac OS e hoje também para o iOS, ambos sistemas operacionais desenvolvidos pela Apple. A linguagem é derivada diretamente do C, com algumas características de Smalltalk, como o uso de parâmetros dentro do nome do método em vez de em uma seção de parâmetros no mesmo.
Vamos fazer agora dar os nossos primeiros passos com o XCode, criando um projeto. Abra o XCode, essa deve ser a primeira janela que você vai ver:
Imagem 1 – Seleção de projetos no XCode
Nessa página, selecione “Create a new Xcode project”, aqui está a próxima janela que você vai ver:
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
4
Imagem 2 – Criando o projeto no Xcode
Nesse primeiro momento, vamos iniciar com uma aplicação para MacOS, pra entender o funcionamento da linguagem e nos acostumarmos com o Xcode como ferramenta. Após selecionar “Mac OS X” -‐> “Application” -‐> “Command Line Tool”, além de selecionar “Foundation” no campo de seleção. Dê o nome “AprendendoObjectivec” ao projeto.
Com o projeto criado, você deve ver uma janela como essa agora:
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
5
Imagem 3 – Visualização inicial do projeto
Um fator interessante ao se iniciar o desenvolvimento usando Xcode é que mesmo se parecendo com pastas, “Source”, “Documentation”, “External frameworks and libraries” e “Products” não são pastas, mas “agrupamentos” de conteúdo. Se você for até a pasta que está o projeto, vai perceber que não existem diretórios equivalentes a eles, isso acontece porque o Xcode organiza os arquivos apenas logicamente e não fisicamente dentro do projeto.
Com o nosso projeto criado, vamos criar um target para os testes unitários que vamos escrever durante o exemplo. Pra fazer isso, clique com o botão direito ou “Control + Click” no marcador “Targets”, siga para “Add”, depois “New Target”. Você deve ver a tela abaixo:
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
6
Imagem 4 – Criando o target que vai rodar os nossos testes
Ao partir pra próxima tela você vai definir o nome do target, coloque o nome Test. Assim que o target for criado, ele vai abrir a janela de opções do mesmo, nela, selecione a aba “General”, clique no botão “+”, você deve ver então uma janela como a imagem logo abaixo, selecione “AprendendoObjectivec” e clique em “Add Target”.
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
7
Imagem 5 – Adicionando o executável da aplicação como dependência ao target Test
Estamos agora finalmente prontos pra começar a escrever o código do projeto. Para entender as construções básicas da linguagem, vamos criar uma classe Conta que guarde os dados de agência, número de conta, banco e saldo. Além disso a classe também vai conter os métodos para sacar, depositar e transferir dinheiro entre contas. Selecione o grupo “Source” e abra o menu contextual. Vá em “Add” -‐> “New File”. Selecione “Cocoa Class” e depois “Objective-‐C class”, como na imagem abaixo:
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
8
Imagem 6 – Criando a nossa primeira classe em Objective-‐C
Na próxima janela, coloque o nome da classe como sendo “Conta.m” e marque o checkbox “Also create ‘Conta.h’”:
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
9
Imagem 7 – Criando a nossa primeira classe em Objective-‐C
Com a classe criada, você deve ver os arquivos no Xcode como na imagem abaixo:
Imagem 8 – visualização do navegador de arquivos com a classe Conta criada.
Olhando pra essa imagem podemos ver que existe um arquivo “Conta.m” e um arquivo “Conta.h”, vejamos o que há de código em cada um desses arquivos:
Listagem 1 – Conta.h
#import <Cocoa/Cocoa.h> @interface Conta : NSObject { } @end
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
10
Se você nunca programou em C na vida, deve estar se perguntando porque temos dois arquivos para uma única classe, um “.h” e outro “.m”. O arquivo “.h”, funciona como “cabeçalho” (daí o “.h”, de “header”), nele você define a interface pública da classe, como os atributos que ela tem e os métodos que ela implementa (e que devem ficar visíveis para terceiros). Em Objective-‐C, diferentemente de Java e outras linguagens, não existem modificadores de nível de visibilidade para as classes ou os seus membros (como métodos e atributos), tudo é “visível”, mas apenas o que estiver definido no arquivo “.h” fica realmente disponível para outros objetos que queiram usar a sua classe. Olhando agora diretamente para o código fonte, vemos o “#import <Cocoa/Cocoa.h>”, isso quer dizer que a nossa classe está declarando que vai utilizar funcionalidades do framework “Cocoa” (na verdade nós só precisamos do framework “Foundation”, mas vamos deixar assim por enquanto). Logo após isso vemos o seguinte código: @interface Conta : NSObject
Sempre que você vir um caractere “@” (arroba) em código escrito em Objective-‐C, quer dizer que o que vem logo após ele é uma extensão da linguagem ao C. Opa, peraí, como assim uma “extensão a linguagem C”? Objective-‐C, assim como C++, existe como uma extensão a linguagem C. Você pode escrever código C dentro de programas escritos em Objective-‐C e o seu código (teoricamente) vai compilar e funcionar normalmente. Os designers da linguagem resolveram então definir uma forma de deixar claro o que não é “C puro” na linguagem usando o caracter “@”. Então sempre que você vir o “@” já sabe que isso é uma extensão do Objective-‐C para adicionar novos comportamentos ao nosso querido e amado C. A extensão @interface diz que estamos definindo uma nova classe na linguagem e o que segue essa declaração é o nome da classe, no nosso caso “Conta”. Logo após a declaração do nome da classe o “: NSObject” diz que a nossa classe “Conta” herda de “NSObject”. Diferentemente de outras linguagens onde existe uma única classe raiz e mãe de todos os objetos (pense no Object de Java, Ruby, C# e tantas outras), em Objective-‐C você mesmo pode definir uma classe raiz, mas normalmente você vai herdar de “NSObject” que é a classe raiz do framework base de Objective-‐C utilizado no desenvolvimento de aplicações para o Mac OS e iOS. Obviamente, se você não disser de qual classe você herda, a sua classe se torna automaticamente uma classe raiz, então lembre-‐se sempre de definir a superclasse da sua classe ou simplesmente coloque que ela herda de “NSObject”. O par de chaves “{}” que vem logo após a declaração da classe é o lugar onde você define as variáveis de instância da sua classe e somente elas (não, não é aqui que você coloca os métodos). Todas as variáveis de instância precisam estar definidas aqui no arquivo “.h”, mesmo aquelas que você queira deixar como “privadas”. Após o par de chaves vem o corpo da classe, que é o lugar onde você define os métodos que essa classe implementa (mas você não os implementa aqui, você
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
11
apenas define quais são eles). Não definir um método aqui normalmente faz com que não seja possível pra que alguém o invoque, é uma das formas de se criar métodos “privados” em Objective-‐C, já que não existe esse conceito dentro da linguagem em si.
Listagem 2 – Conta.m
#import "Conta.h" @implementation Conta @end
No arquivo “.m” você encontra agora o código real da classe (mesmo que não tenhamos colocado nada ainda nele. Enquanto que no arquivo “.h” nós havíamos definido a @interface do código, agora estamos definindo a @implementation. Veja que aqui não é mais necessário definir de qual classe a nossa classe herda, a definição fica apenas na interface. Veja que o código também faz um “#import” para o arquivo “.h”, isso é para que o arquivo de implementação possa ver as informações definidas na interface, como variáveis de instância e também receber automaticamente as dependências que já foram importadas no mesmo. Em C você faria o mesmo com #include, mas o #import vai um pouco mais longe e evita que o mesmo arquivo seja “incluído” duas vezes, um problema bem comum pra quem trabalha com C. Vamos agora começar a realmente escrever código:
Listagem 3 – Conta.h
#import <Cocoa/Cocoa.h> @interface Conta : NSObject { float saldo; } - (BOOL) depositar:(float)valor; - (float) saldo; @end
Agora a nossa classe conta tem uma variável de instância definida (do tipo “float”), declaramos a existência do método depositar que recebe um parâmetro do tipo float e retorna um BOOL, o boolean da linguagem. Também declaramos o método “saldo” que vai ser a forma de acessar a variável de instância “saldo”. É possível acessar uma variável de instância de uma classe em Objective-‐C diretamente de “fora” da classe, mas o melhor é fazer o acesso sempre via métodos (ou as propriedades que veremos mais a frente). O sinal de “-‐“ (subtração) antes da definição do método avisa que esse é um método de instância (métodos de classe são definidos com um sinal de adição, o “+”). O tipo de retorno e o tipo dos parâmetros recebidos ficam sempre entre parênteses. Um detalhe importante na declaração de métodos em Objective-‐C é que o parâmetro fica “dentro” do nome do método e não após a definição do nome,
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
12
como em Java ou Ruby. Como o método depositar recebe um parâmetro é necessário colocar os “:” para separar o nome do parâmetro que está sendo passado, depois vamos entender um pouco mais como se nomeiam métodos em Objective-‐C. Vejamos então como vai ficar o nosso arquivo “.m”:
Listagem 4 – Conta.m
#import "Conta.h" @implementation Conta - (BOOL) depositar: (float) valor { if ( valor > 0 ) { saldo += valor; return YES; } else { return NO; } } - (float) saldo { return saldo; } @end
Olhando pro código fonte você já deve ter entendido exatamente o que ele faz, se o valor passado como parâmetro for maior do que zero, ele vai somar com o valor atual da variável de instância “saldo” e retornar YES, que é um atalho para o valor BOOL que representa verdadeiro, se o valor passado como parâmetro for menor ou igual a zero ele simplesmente retorna NO. Assim como em C, blocos de código em Objective-‐C ficam sempre dentro de pares de chaves (“{}”) e todas as estruturas de controle que você conhece do C (ou Java e C#) existem exatamente da mesma forma em Objective-‐C. A implementação do método “saldo” é ainda mais trivial, ela simplesmente retorna o valor da variável de instância diretamente.
Criando o nosso primeiro teste unitário
Com a nossa classe implementada, agora é a hora de escrever um teste para esse primeiro método implementado. A primeira coisa a se fazer é criar um novo grupo dentro do projeto pra manter as classes de teste, assim podemos facilmente separar as classes dos seus testes na hora de gerar a aplicação final, já que ela não precisa levar os testes consigo. Clique com o botão direito em “AprendendoObjectivec”, como na imagem abaixo:
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
13
Imagem 9 – Criando o grupo “Test”
Com o grupo criado, clique com o botão direito em “Test”, selecione “Add” e “New File”. Você vai criar uma classe de teste padrão do Cocoa, como na imagem abaixo:
Imagem 10 – Criando a classe de testes
Dê o nome “ContaTest” a classe e selecione o Target “Test” apenas, desmarcando o primeiro Target que aparece, como na imagem abaixo:
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
14
Imagem 11 – Selecionando os targets da classe de testes
No caso dos testes, você normalmente não precisa definir nada no arquivo “.h”, já que os métodos são chamados automaticamente, então vamos nos focar apenas no arquivo “.m”, que é onde nós vamos realmente estar trabalhando.
Implementando o nosso primeiro teste
Listagem 5 – ContaTest.m
#import "ContaTest.h" #import "Conta.h" @implementation ContaTest - (void) testDepositarComSucesso { Conta * conta = [[Conta alloc] init]; [conta depositar:200]; STAssertTrue( [conta saldo] == 300, @"O saldo deve ser de 300 para que o teste falhe" ); } @end
Assim como em outros frameworks de teste, o método que define a implementação do teste precisa ter o seu nome iniciando com “test” e não retornar nada (por isso o void). Na primeira linha do teste já temos vários detalhes da linguagem pra entender. A primeira coisa a ser percebida é que a declaração da variável contém um “*”, se você vem do C, sabe que isso quer dizer que essa variável é na verdade uma referencia (ou ponteiro) pra um objeto que está em memória. Sempre que você define uma referencia pra uma variável em Objective-‐C é necessário colocar o
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
15
“*”, se você não o fizer vai receber warnings do compilador e o seu programa não vai funcionar corretamente. Ainda nessa linha temos o idioma de inicialização de objetos. O que em outras linguagens de programação OO seria feito através de um construtor, em Objective-‐C se faz através das chamadas a “alloc”, um método de classe que organiza uma nova instância do objeto em memória e retorna o objeto pronto pra uso, e “init”, que executa finalmente a inicialização do objeto. É possível usar também o método de classe “new” em vez do “par” alloc/init para se criar um objeto, mas é preferível usar alloc/init, especialmente se você pretende ter várias formas de inicializar o seu objeto. Ótimo, você diz, mas o que diabos são aqueles pares de colchetes (“[]”)? Seguindo a tradição de Smalltalk, em Objective-‐C a ideia não é que você está chamando um método em um objeto, mas sim enviando uma mensagem a ele. Inicialmente, a sintaxe pode realmente parecer um pouco estranha, mas no fim das contas ela é bem simples: [ objetoDestino mensagem ] Do lado esquerdo, você sempre tem o objeto para o qual você quer enviar a mensagem, do lado direito você tem a mensagem que está sendo enviada (o método que está sendo chamado), tudo isso dentro de colchetes. No nosso caso, onde fazemos “[[Conta alloc] init]”, estamos enviando a mensagem “alloc” para o objeto que representa a classe “Conta” e no valor que é retornado por esse método (um objeto Conta) fazemos a chamada do método “init”. O idioma alloc/init é comum e pervasivo em toda a linguagem e exemplos de código que você vai encontrar, mas evite fazer chamadas de método dentro de chamadas de método no seu código, a não ser que seja um caso muito simples como esse que nós estamos vendo aqui. Na segunda linha do teste vemos o seguinte: [conta depositar:200];
Nós enviamos a mensagem “depositar” para o objeto representado pela variável “conta” com o parâmetro 200. Os “:” que nós usamos na definição do método “depositar” fazem realmente parte do nome do método, sendo obrigatório a sua adição a chamada (os métodos que não adicionam “:” são os que não recebem parâmetros). No fim temos o código que faz a asserção do teste: STAssertTrue( [conta saldo] == 300, @"O saldo deve ser de 300 para que o teste falhe" );
Comparamos então o valor do saldo com 300 exatamente porque queremos, nesse primeiro momento, ver o teste falhar. STAssertTrue não é um método, mas uma função comum que você definiria como uma função em C, ela faz parte do framework de testes unitários que vem por padrão dentro do Cocoa. Agora um detalhe importante que pode passar desapercebido é a definição do texto usado como mensagem para esse teste, em vez de ser somente um conjunto de caracteres entre aspas, há um “@” antes das aspas. Isso quer dizer que você
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
16
não está usando uma string comum do C, que é um array de caracteres, e sim um objeto do tipo NSString que adiciona várias funcionalidades as strings comuns do C. A maior parte do código em Objective-‐C que lida com Strings vai esperar que você envie objetos do tipo NSString, então é bom se acostumar a escrever primeiro a “@” antes de declarar um string no seu código. Voltemos agora para o Xcode, onde você vai mudar o Target padrão para Test, veja como fazer isso na imagem abaixo:
Imagem 12 – Alterando o Target padrão para Test
Agora estamos entrando no modo de testes do Xcode vamos poder começar a executar os testes, mas antes de fazer isso precisamos dizer para o Target “Test” onde ele vai achar a classe Conta, pois ela não foi adicionada a ele. Pra fazer isso, você deve selecionar o arquivo “Conta.m” e arrastá-‐lo para dentro da pasta “Compile Sources” do Target “Test”, como na imagem abaixo:
Imagem 13 – Drag e drop do arquivo “Conta.m” em Test
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
17
Com isso feito, podemos finalmente executar o build do projeto, pra fazer isso digite “Command + B”. Após alguns instantes o Xcode deve terminar de fazer o build e você deve ver um aviso no canto inferior direito da ferramenta indicando que existem dois erros:
Imagem 14 – Indicação de erros de build do Xcode
Ao clicar nos erros você deve ver a seguinte tela:
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
18
Imagem 15 – tela de erros no build do Xcode
Como você pode perceber, o erro na verdade não é exatamente no build, mas sim no nosso teste unitário que falhou, já que o valor da variável não é 300 e sim 200. Para ver essa tela sem ter que usar o mouse pra clicar no erro basta fazer “Shift + Command + B”. Agora que você já viu a tela, troque o 300 por 200 e execute o build mais uma vez com “Command + B”, o seu build deve executar sem erros, agora que o teste já está implementado corretamente. Vamos agora definir os métodos sacar e transferir na classe conta:
Listagem 6 – Conta.h
#import <Cocoa/Cocoa.h> @interface Conta : NSObject { float saldo; } - (BOOL) depositar: (float) valor; - (BOOL) sacar: (float) valor; - (BOOL) transferir: (float) valor para: (Conta *) destino; - (float) saldo; @end
Estamos quase chegando lá, o método sacar tem a definição igual a depositar, mas o método “transferir:para:” (veja só como ele se chama) deve estar dando um nó no seu cérebro nesse momento. Vejamos: - (BOOL) transferir: (float) valor para: (Conta *) destino;
Em Objective-‐C, quando você tem um método que recebe vários parâmetros, você precisa “dividir” o nome do método em pedaços para receber os
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
19
parâmetros. Então em vez de simplesmente fazer um “transferir( double valor, Conta destino )”, como você faria em Java ou C#, você quebra o nome do método e transformar ele em “transferir: valor para: destino”. Inicialmente a sintaxe parece estranha, mas a própria leitura da chamada do método fica mais simples e se você tiver um método que recebe vários parâmetros ele com certeza vai ficar bem mais legível, já que cada parâmetro vai ter o seu identificador antes. Vejamos agora como fica a implementação desses dois métodos:
Listagem 7 – Conta.m
#import "Conta.h" @implementation Conta - (BOOL) depositar: (float) valor { if ( valor > 0 ) { saldo += valor; return YES; } else { return NO; } } - (BOOL) sacar:(float)valor { if ( valor > 0 && valor <= saldo) { saldo -= valor; return YES; } else { return NO: } } - (BOOL) transferir:(float) valor para:(Conta *) destino { if ( [self sacar: valor] && [ destino depositar: valor ] ){ return YES; } else { return NO; } } - (float) saldo { return saldo; } @end
A essa altura do campeonato, você já sabe exatamente o que esse código todo está fazendo, então vamos passar diretamente pros testes, pra ver esses novos
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
20
métodos sendo exercitados. Em todos os nossos testes vamos, utilizar um objeto conta, então em vez de recriar esse objeto a cada teste, vamos ser inteligentes definir uma variável de instância Conta no nosso teste e implementar o método “setUp” que cria a conta para não repetirmos essa operação:
Listagem 8 – ContaTest.h
#import <SenTestingKit/SenTestingKit.h> @class Conta; @interface ContaTest : SenTestCase { Conta * conta; } @end
Adicionamos a definição da variável de instância na classe, como esperado, mas o que é esse “@class” que também está aí? O @class em Objective-‐C é uma “forward reference” e normalmente é utilizado em arquivos “.h” para que você diga que o seu arquivo depende de uma classe em específico, mas não vai fazer o “#import” dessa classe aqui, vai deixar pra importar o arquivo da classe somente no seu “.m”. Se você não colocar o @class nem o “#import” pra o arquivo da classe não vai ser possível compilar o código. Um detalhe importante do “#import” é que quando o texto que vem após ele está entre “<>” (como em #import <SenTestingKit/SenTestingKit.h>), isso indica ao compilador que ele deve procurar esse arquivo no “load path” do sistema operacional, os lugares onde ficam os arquivos “.h” do mesmo. Quando o “#import” aparece usando aspas no conteúdo a ser importado, quer dizer que ele deve procurar dentro dos arquivos locais do projeto (como em #import "Conta.h"). Vejamos agora a implementação atual dos testes:
Listagem 9 – ContaTest.m
#import "ContaTest.h" #import "Conta.h" @implementation ContaTest - (void) setUp { conta = [[Conta alloc] init]; [conta depositar: 200]; } - (void) testDepositarComSucesso { [conta depositar:150]; STAssertTrue( conta.saldo == 350, @"Saldo final deve ser 350" );
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
21
} - (void) testDepositarComFalha { [conta depositar:-150]; STAssertTrue( conta.saldo == 200, @"Valor do saldo não deve ter se modificado" ); } - (void) testSacarComSucesso { [conta sacar:150]; STAssertTrue( conta.saldo == 50, @"O saldo atual deve ser 50" ); } - (void) testSacarComValorMaior { [conta sacar: 250]; STAssertTrue( conta.saldo == 200, @"O saldo atual não deve ter se modificado" ); } - (void) testSacarComValorNegativo { [conta sacar: -100]; STAssertTrue( conta.saldo == 200, @"O saldo atual não deve ter se modificado" ); } - (void) testTransferirComSucesso { Conta * destino = [[Conta alloc] init]; [conta transferir:150 para: destino]; STAssertTrue( conta.saldo == 50, @"O saldo da conta origem deve ser 50" ); STAssertTrue( destino.saldo == 150, @"O saldo da conta destino deve ser 250" ); } - (void) testTransferirComFalha { Conta * destino = [[Conta alloc] init]; [ conta transferir:250 para: destino ]; STAssertTrue( conta.saldo == 200, @"O saldo da conta origem deve ser 50" ); STAssertTrue( destino.saldo == 0, @"O saldo da conta destino deve ser 250" );
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
22
} @end
Temos então vários testes para a implementação das funcionalidades da nossa classe conta, eles são bem simples, mas tem duas coisas importantes que não discutimos ainda, a primeira é essa: conta.saldo == 200
Antes, no nosso teste, fazíamos a chamada assim: [conta saldo] == 200
Em Objective-‐C, se você tem um método que não recebe parâmetros e retorna um valor, esse método pode ser chamado como se ele fosse uma propriedade do seu objeto, sem que você tenha que fazer uma invocação explícita do mesmo, então “conta.saldo” é a mesma coisa que escrever “[conta saldo]”, o compilador vai fazer a mágica de transformar o primeiro no segundo pra você. Já o segundo caso: [ conta transferir:250 para: destino ]
Aqui nós vemos um exemplo da chamada do método “transferir:para:”, junto com o transferir nós temos o valor que vai ser transferido e logo depois de para temos o objeto que vai receber a transferência, veja que não existem vírgulas separando os parâmetros, eles são separados normalmente pelos espaços entre os nomes que formam o método e os parâmetros passados.
Criando um “construtor” para o nosso objeto conta
Como já comentamos antes, o par “[[Conta alloc] init]” serve pra criar o objeto e inicializá-‐lo dentro do projeto, mas e se nós quisermos definir uma forma personalizada? Simples, criamos um método de inicialização. No nosso caso queremos poder criar contas com um saldo que seja diferente de zero. Começamos por definir o método “initWithSaldo” em “Conta.h”:
Listagem 10 – Conta.h – declarando o método initWithSaldo
#import <Cocoa/Cocoa.h> @interface Conta : NSObject { float saldo; } - (Conta *) initWithSaldo: (float) valor; - (BOOL) depositar: (float) valor; - (BOOL) sacar: (float) valor; - (BOOL) transferir: (float) valor para: (Conta *) destino; - (float) saldo; @end
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
23
Como não existem construtores realmente dentro da linguagem, o que nós fazemos é definir métodos que inicializem o objeto com o valor que nos interessa, vejamos a implementação agora pra entender um pouco mais o que está acontecendo e porque é necessário criar esses métodos:
Listagem 11 – Conta.m
#import "Conta.h" @implementation Conta - (Conta *) initWithSaldo:(float)valor { if ( self = [ self init]) { saldo = valor; } return self; } // todo aquele código que você já viu @end
Mais um caso de idioma da linguagem surge então pra nós nesse momento: if ( self = [ self init]) {
Por mais estranho que pareça, esse código está realmente atribuindo um valor a variável “self” (“self” é equivalente ao “this” em Java e C#, é uma referência para o objeto onde o método em execução atual foi chamado) dentro do seu objeto. Mas porque alguém iria querer fazer isso? Na implementação da Apple do Objective-‐C existe um conceito chamado de “class-‐clusters”. Quando você cria um objeto do tipo NSString, o que você recebe pode não ser exatamente um NSString, mas uma subclasse dele. As classes que nós vemos “do lado de fora” funcionam apenas como um meio pra se acessar as classes que fazem realmente o trabalho, mas esses detalhes de implementação ficam escondidos graças a essa pequena mágica do método init (atribuir um novo valor a self e retornar esse valor). No caso da nossa classe não seria necessário fazer essa mágica, já que estamos realmente retornando uma conta, mas o ideal é que você construa todos os seus inicializadores dessa forma para que quando for criar uma subclasse de uma classe padrão da linguagem não se esquecer e terminar com bugs estranhos no seu código. Outra coisa importante também é lembrar-‐se de chamar o método “init” da sua classe, se você estiver implementando um novo inicializador (como nós fazemos nesse código) ou chamar o método init da sua superclasse (com [super init]) se você estiver sobrescrevendo o método “init”, assim você não perde o código de inicialização que já tenha sido implementado na classe atual e nas suas superclasses.
Definindo propriedades automaticamente nos objetos
Nós vimos que o compilador é inteligente o suficiente pra aceitar que “[conta saldo]” seja chamado como “conta.saldo”, mas além disso nós também podemos
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
24
instruir o compilador para que ele gere os métodos de acesso para as propriedades dos nossos objetos automaticamente (pense no “generate getters and setters” do Eclipse ou attr_accessor em Ruby). Pra isso, vamos definir novas propriedades na nossa classe, “agencia” e “conta”. Vejamos como fica o nosso “Conta.h” agora:
Listagem 12 – Conta.h com as novas propriedades definidas #import <Cocoa/Cocoa.h> @interface Conta : NSObject { float saldo; NSString * conta; NSString * agencia; } @property (copy, nonatomic) NSString * conta; @property (copy, nonatomic) NSString * agencia; @property (readonly) float saldo; - (Conta *) initWithSaldo: (float) valor; - (BOOL) depositar: (float) valor; - (BOOL) sacar: (float) valor; - (BOOL) transferir: (float) valor para: (Conta *) destino; @end
Nós definimos as duas variáveis de instância, “agencia” e “conta”, e logo depois temos as declarações das propriedades, com “@property”: @property (copy, nonatomic) NSString * conta;
Isso indica ao compilador que nós vamos ter os métodos abaixo definidos: -‐ (NSString * ) conta; -‐ (void) setConta: ( NSString * );
As instruções “copy” e “nonatomic” são atributos que vão ser utilizados na geração da implementação dos métodos.
-‐ “copy” indica que quando um objeto for recebido como parâmetro, deve ser criada uma cópia desse objeto e essa cópia é quem vai ser atribuída a variável de instância, isso é necessário especialmente se você está recebendo dados da interface, pois os strings que vem dela podem ser recolhidos da memória a qualquer momento nos ambientes onde não há coletor de lixo, como iPads e iPhones.
-‐ “nonatomic” indica que os métodos gerados não vão fazer nenhum controle sobre o acesso de forma concorrente. Esse normalmente é o caso pra maior parte das aplicações, mas se você vai utilizar esse objeto em um ambiente com concorrência, onde várias threads vão acessar o mesmo objeto e chamar seus métodos, deve remover isso.
-‐ “readonly” indica que apenas o método “getter”, que lê o valor da variável, vai ser gerado, o método set, que altera o valor da variável não vai ser gerado.
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
25
Com os métodos definidos, agora voltamos pra “Conta.m” pra declarar o código que vai fazer com que os métodos sejam realmente gerados, vejamos como fica o código agora:
Listagem 13 – Conta.m com a definição das propriedades e gerenciamento de memória
#import "Conta.h" @implementation Conta @synthesize agencia, conta, saldo; - (void) dealloc { [ self.agencia release ]; [ self.conta release ]; [ super dealloc ]; } // todo o resto do código que você já conhece aqui @end
A mágica de verdade vive no “@synthesize”, ele instrui o compilador a ler as informações das propriedades definidas através de “@property” e gerar os métodos. Junto com isso chegamos a um detalhe importante da programação com Objective-‐C, especialmente se você estiver planejando programar para iOS, o gerenciamento de memória. Com a definição das propriedades nós definimos também o método “dealloc” que é chamado quando um objeto vai ser removido da memória, para que ele possa limpar da memória também os objetos para os quais ele aponta. Quando você está programando em Objective-‐C para iOS, não existe um coletor de lixo automático, liberar a memória é responsabilidade do programador, então é necessário que você tome cuidado para não vazar memória no seu código e estourar a memória do dispositivo. Em Objective-‐C o controle de memória, quando não é feito através do coletor de lixo, acontece através da contagem de referencias. Sempre que você cria um objeto usando “alloc” ou “new” esse objeto fica com o contador de referencias em 1 (um), cada vez que você chama “retain” no objeto (como em “[objeto retain]”) esse contador aumenta em um e cada vês que você chama “release” (como em “[objeto release]”) o contador diminui em 1. Quando o contador de referencias atingir 0, o objeto é removido da memória. No nosso caso, as propriedades “conta” e “agencia” são definidas como “copy”, isso quer dizer que o objeto que vai ser colocado na variável de instância vai ser clonado e o clone é o objeto que vai finalmente ficar disponível para a nossa classe. Como o clone é um objeto recém-‐criado com base em outro objeto a sua contagem de referencias é 1 (um), o que quer dizer que quando chamarmos “release” neles, eles vão ser removidos da memória. Pra limpar a memória que estamos usando, precisamos ajustar o nosso teste para fazer o “release” dos objetos conta que estão sendo criados. Vamos fazer o
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
26
release da variável de instância conta no método tearDown e da conta destino em cada um dos testes, vejamos o que muda na nossa classe teste:
Listagem 14 – ContaTest.m com o código de gerenciamento de memória
@implementation ContaTest //métodos não mostrados aqui não foram alterados - (void) tearDown { [ conta release ]; } - (void) testTransferirComSucesso { Conta * destino = [[Conta alloc] initWithSaldo: 100 ]; [conta transferir:150 para: destino]; STAssertTrue( conta.saldo == 50, @"O saldo da conta origem deve ser 50" ); STAssertTrue( destino.saldo == 250, @"O saldo da conta destino deve ser 250" ); [ destino release ]; } - (void) testTransferirComFalha { Conta * destino = [[Conta alloc] init]; [ conta transferir:250 para: destino ]; STAssertTrue( conta.saldo == 200, @"O saldo da conta origem deve ser 50" ); STAssertTrue( destino.saldo == 0, @"O saldo da conta destino deve ser 250" ); [ destino release ]; } - (void) testSetContaEAgencia { conta.agencia = @"1111-0"; conta.conta = @"10.000-9"; STAssertEqualObjects( conta.agencia, @"1111-0", @"O valor deve ser igual" ); STAssertEqualObjects( conta.conta, @"10.000-9", @"O valor deve ser igual" ); } @end
Curso de desenvolvimento de aplicações para iOS usando Objective-‐C Maurício Linhares
27
Adicionamos a nossa implementação o método “tearDown” que envia um “release” para o objeto conta e também fazemos o release dos objetos destino criados nos testes de transferência. Você nunca deve chamar o método dealloc diretamente nos seus objetos, sempre chame release e deixe que o próprio runtime do Objective-‐C vai fazer a chamada a dealloc quando for a hora correta. É possível definir algumas regrinhas básicas na hora de lidar com a gerência de memória em aplicações escritas em Objective-‐C:
• Se você pegou um objeto através de “alloc/new/copy”, esse objeto tem um contador de 1 e você deve se lembrar de liberar esse objeto quando ele não for mais necessário;
• Se você pegou um objeto de outro lugar, assuma que ele tem um contador de 1, se você só vai usá-‐lo e deixar ele pra lá, não faça nada com ele, quem passou ele pra você provavelmente vai limpá-‐lo quando for necessário.
• Se você precisa manter um objeto recebido de outro lugar para usá-‐lo em outro momento, chame “retain” nele para que o contador aumente para “2”, assim quando quem lhe passou esse objeto chamar “release” nele o contador vai baixar pra “1” e o objeto ainda não vai ser liberado da memória.
• Sempre que você dá “retain” em um objeto, deve garantir que vai dar um “release” nele em algum momento, se você não der o “release”, com certeza vai estar vazando memória na sua aplicação;
Gerenciamento de memória é um tópico longo e vamos nos aprofundar mais nele conforme avançamos para a construção das nossas aplicações para iOS, essa introdução é apenas para que você entenda o básico de como esse conceito funciona dentro da linguagem.