Podcast no bar!Aprendendo Objective-C

Eu já gravei um podcast com uns amigos e, acreditem ou não, adivinhei que o Al Pacino era o assassino do filme “Righteous Kill” (duas faces da lei) antes do filme sair (opa, você não tinha assistido? Bom… não é um spoiler quando o filme é ruim). A gravação foi divertida, nós tentamos organizar como o programa iria funcionar, assim como a pauta e a sequência em que cada tópico seria discutido (basicamente era um papo sobre Robert De Niro & Al Pacino).

Não seria legal se eu pudesse encontrar uns amigos em um bar, gravar uma conversa sobre algo e logo depois remover as partes chatas da conversa, inserir umas vinhetas/sons engraçados e umas músicas de fundo? Tudo isso de forma simples e com uma interface amigável? Claro que, a priore, a qualidade do áudio não ficaria 100%, mas iria possibilitar a criação de podcasts para leigos e principalmente ajudar aqueles que, assim como eu,  tem preguiça de editar gravações.

"Na natureza nada se cria, nada se perde, tudo se transforma" --Lavoisier, cientista e pederasta

O maior desafio no mercado de desenvolvimento IOS (iPhone, iPad, iPod touch.. e Mac Os) é que pode ser muito difícil pensar em algo que já não tenha sido criado. Então a idéia é adicionar mais features ou simplificar; moldar a idéia principal com algum diferencial que torne sua aplicação atraente.

Bom, pensando nisso tudo, resolvi escrever um tutorial sobre programação com Objective-C. Nos posts a seguir vou tentar montar uma aplicação passo-a-passo e compartilhar minhas descobertas no mundo do desenvolvimento IOS.

Como venho do desenvolvimento Java, fiquei muito impressionado com os detalhes esquisitos da linguagem. Portanto, quando você ver esta imagem IOS_dummyDetails, significa que o trecho de código merece uma merece uma atenção especial.

Ok, vamos começar! Nós vamos precisar de:

1) Um Macbook (Ou uma VM, google ‘vmware + Mac OS X 10.6.6’). No meu caso eu resolvi comprar um MacBook White e acabei abandonando meu Dell Vostro 1510 + OpenSuse 11.2. No Mac OS  tudo é muito smooth.. é um outro mundo, eu recomendo.

2) Xcode 4 (Copie de algum colega que pagou os $99 do cadastro de desenvolvedor ou baixe da App Store por $5. Dica: Copie o .dmg e descompacte via linha de comando: hdiutil).

Para a aplicação funcionar, ela tem que gravar a voz do usuário através do iPad, então vamos construir uma aplicação simples que inicia e para a gravação através de um botão. Inicie seu Xcode e crie um novo projeto do tipo “View-Based Application”:

xcode app type

Uma série de arquivos serão criados automaticamente (em uma estrutura virtual, não física), os mais importantes são:

PodcastAtTheBarAppDelegate.h - header da classe AppDelegate
PodcastAtTheBarAppDelegate.m - implementações de métodos da AppDelegate 
* Toda aplicação de IOS tem um objeto (singleton) do tipo UIApplication. Isto fornece algumas funcionalidades básicas para executar e gerenciar uma aplicação. Ações enviadas a UIApplication são delegadas para a AppDelegate, desta forma podemos fazer nossos próprios tratamentos de eventos como:  applicationDidfinishLauching, applicationDidBecomeActive, etc.
PodcastAtTheBarViewController.h - Arquivo para declarar assinatura de métodos.
PodcastAtTheBarViewController.m - Classe principal onde ficarão os métodos da nossa camada de controle.
PodcastAtTheBarViewController.xlib - Arquivo XML da estrutura da view (interface gráfica), tela da app.

Clique (apenas 1 vez) no arquivo .xlib e vamos usar o interface builder e a biblioteca de objetos para criar nossa tela, neste primeiro momento ela terá apenas um simples botão “Gravar”. Clique no ‘Round Rect Button’ na biblioteca de objetos e arraste para a tela do seu iPad:

RoundRectButton

Agora adicione o seguinte método na sua classe “PodcastAtTheBarViewController.m”:

  1. - (IBAction)
  2. criarEntradaDeLogQuandoAlguemClicarNoBotao {
  3.   NSString *msg = @"ALGUÉM clicou no botão";
  4.   NSLog([msg lowercaseString]);
  5. }

Vamos tentar explicar um pouco a sintaxe deste código: nas duas primeiras linhas nós temos a assinatura do método , o símbolo ‘-‘ é usado para definir métodos de instância (para métodos de classe usa-se o ‘+’). Na linha 3 temos a declaração de uma NSString (Objeto String do Objective-C. É chamado assim por causa da biblioteca da linguagem que foi trazida da empresa comprada pela Apple, a Next/NextStep), veja que temos um asterisco (*msg), isto acontece porquê estamos declarando um ponteiro para o objeto NSString (você pode aprender mais sobre ponteiros aqui). Mais um detalhe, este objeto string possui um arroba (@) para determinar que esta String é uma NSString.

IOS_dummyDetailsNa linha 4 temos a chamada para o método NSLog (veja que o ‘Quick Help’ sempre aparece no canto superior direito da tela com detalhes sobre Classes e Métodos que você está usando), passamos o objeto da mensagem dentro do NSLog. Neste exemplo estou usando o método ‘lowerCaseString’, se você deseja conhecer outros: que tal experimentar? Digite ‘[msg ‘ e use o recurso de auto-complete do XCode, quer saber o atalho? É bastante intuitivo: ESC ! É isso mesmo, você deve teclar ‘esc’ para ver as opções de auto-complete. E o que você achou da sintaxe para invocar métodos e acessar variáveis? Ao invés de algo decente como ‘msg.toLowerCase()’, usamos colchetes para cercar o objeto e seu método ([msg lowercaseString]), e isso fica ainda melhor quando utilizamos um método a partir de um objeto retornado de outro método, e.g.: NSString *bundlePath = [[[NSBundle mainBundle] resourcePath];

IMPORTANTE! Declare o método sem implementação (como um método abstrato) no arquivo ‘PodcastAtTheBarViewController’, os headers devem ser usados para que sua aplicação conheça os recursos que podem utilizar, se houvesse uma chamada para este método na classe .m antes da declaração do método, o mesmo não seria reconhecido. Então SEMPRE lembre de declarar tudo nos headers (mesmo que o faça depois de escrever a implementação). Assim, no final de nosso arquivo .h teremos:

  1. -(IBAction)mudarLabelQuandoOCaraClicarNoBotao;
  2. @end

Você ainda quer continuar? Ok, vamos mostrar mais uma pérola… Você deve estar se perguntando: “Mas como eu sei que este método será chamado quando eu clicar naquele botão específico?”, boa pergunta. A imagem abaixo mostra como “linkar” um evento do botão com um determinado método, após clicar com o botão direito no ‘Round Rect Button’ veremos uma lista de eventos, vamos clicar no pequeno círculo a direita do evento ‘Touch Up inside’ (algo próximo de ‘on release’) e arrastar, veja que uma linha azul aparece, arraste esta linha até o icone do cubo amarelo (File`s owner) e solte, você deverá ver a lista de métodos declarados, selecione o método ‘criarEntradaDeLogQuandoAlguemClicarNoBotao‘ e sua aplicação estará pronta para o primeiro teste.

linkingUIObjectWithMethod

Agora execute o simulador (⌘ + R) e clique no botão:

Simulador

Você deve ver uma saída parecida com essa:

  2011-05-31 22:53:07.155 PodcastAtTheBar[827:207] alguém clicou no botão

Agora vamos dar uma olhada na biblioteca que vai trabalhar com Audio:

http://developer.apple.com/library/ios/#documentation/AVFoundation/
Reference/AVAudioRecorder_ClassReference/

Ok, vamos inserir esta biblioteca no nosso projeto:

1) Clique no ícone de seu projeto que se encontra no menu do lado esquerdo, uma janela deve surgir, clique na aba “Build Phases”.

2) Clique em ‘Link Binary With Libraries’ e use o botão + para adicionar a nossa biblioteca ‘AVFoundation.framework’:

AVFoundation

Agora precisamos declarar algumas coisas que serão usadas no nosso header (PodcastAtTheBarViewController.h):

    1. #import <UIKit/UIKit.h>
    2. #import <AVFoundation/AVFoundation.h>
    3. @interface recordViewController : UIViewController
    4.   <AVAudioRecorderDelegate, AVAudioPlayerDelegate>
    5.   {
    6.          AVAudioRecorder *audioRecorder;
    7.          AVAudioPlayer *audioPlayer;
    8.          UIButton *recordButton;
    9. }
    10. @property (nonatomic, retain) IBOutlet UIButton *recordButton;
    11. -(IBAction) recordAudio;
    12. @end

Novamente, vamos tentar entender a sintaxe: Nas linhas 1 e 2 nós temos imports de outros headers. Na linha 3 temos a declaração da nossa classe ‘recordViewController’ (sim, classes são declaradas com a notação @interface) que estende(:) ‘UIViewController’. As classes declaradas entre ‘<>’ representam, o que chamamos em Java, Interfaces, mas em objective-c é chamado de @protocol. Essas classes são frequentemente usadas como classes “Delegate”, ou seja, são classes as quais eventos serão delegados (permitindo que seja feito override/implementação de seus métodos para realizar ações específicas), nós já temos um exemplo concreto que é a classe criada junto com nosso projeto (PodcastAtTheBarAppDelegate.h). Veja as seguintes linhas desta classe:

  1. @class PodcastAtTheBarViewController;
  2. @interface PodcastAtTheBarAppDelegate : NSObject <UIApplicationDelegate> {

Veja que o objeto pai é NSObject e que ela implementa a classe ‘UIApplicationDelegate’ para servir como ponto de implementação dos métodos invocados pela “UIApplication” (como discutido anteriormente neste post).

IOS_dummyDetailsMais um detalhe interessante na linha 1: ao declarar uma classe, usamos @interface, mas quando estamos importando uma classe usamos @class.

Voltando a classe ‘PodcastAtTheBarViewController.h’, nas linhas 6,7 e 8 criamos ponteiros para objetos que serão necessários na implementação dos botões. Na linha 10 declaramos um ponteiros referente ao botão como proprieade, mas os ‘getters’ e ‘setters’ para este ‘atributo’ da classe só será sintetizado depois. Os objetos são referenciados com um recurso do InterfaceBuilder chamado ‘IBOutlet’ e cada uma destas propriedades possui as instruções ‘nonatomic’ e ‘retain’. Explicando:

– nonatomic: Significa que quando os getters e setters da propriedade forem gerados através do @synthetize (na classe de implementação, arquivo .m), eles não serão implementados usando nenhum lock. Então, quando acessados, os valores desta propriedade podem ser mudados a qualquer momento e não vai haver nenhum bloqueio (quem vem de Java deve estar lembrando do modificador ‘synchronized’), i.e.: os acessos não são serializados.

– retain: Vai incrementar o contador de objetos retidos para esta propriedade, desta forma ela não será desalocada quando ela sair do escopo. Usando esta abordagem será necessário usar o ‘dealloc’ para liberar memória. Saiba mais aqui.

Logo depois temos a declaração do método (-(IBAction) recordAudio) e o fim da classe(@end).

* Importante: Lembre-se também de referenciar o botão como Outlet no ‘PodcastAtTheBarViewController.xib’, clique com o botão direito no botão ‘Gravar’ e arraste a linha azul a partir da seção ‘Referencing Outlets’ até o File`s owner, selecione o UIButton que você declarou: recordButton. Associe também o evento ‘Touch up inside’ ao novo método ‘recordAudio’.

Precisamos que um objeto AVAudioRecorder esteja disponível ao iniciarmos nossa aplicação, para isso utilizamos o seguinte método:

  1. – (void)viewDidLoad
Veja mais na documentação: http://developer.apple.com/library/ios/#documentation/AVFoundation/Reference/
AVAudioRecorder_ClassReference/Reference/Reference.html#//apple_ref/occ/instm/AVAudioRecorder/
initWithURL:settings:error:
Então o método ‘viewDidLoad’ da classe ‘PodCastAtTheBarViewController.m’ ficará assim:
  1. – (void)viewDidLoad {
  2.            [super viewDidLoad];
  3.            _
  4.            _
  5.            NSString * filePath = [NSHomeDirectory()
  6.                       stringByAppendingPathComponent:@”Documents/recording.caf”];
  7.            NSDictionary *recordSettings = [NSDictionary
  8.                       dictionaryWithObjectsAndKeys:
  9.                       [NSNumber numberWithInt:AVAudioQualityMin],
  10.                       AVEncoderAudioQualityKey,[NSNumber numberWithInt:16],
  11.                       AVEncoderBitRateKey,
  12.                       [NSNumber numberWithInt: 2],
  13.                       AVNumberOfChannelsKey,
  14.                       [NSNumber numberWithFloat:44100.0],
  15.                       AVSampleRateKey,
  16.                       nil];
  17.            NSError *error = nil;
  18.            audioRecorder = [[AVAudioRecorder alloc] initWithURL: [NSURL fileURLWithPath:filePath]
  19.                       settings: recordSettings
  20.                       error: &error];
  21.            if (error)
  22.            {
  23.                       NSLog(@”error: %@”, [error localizedDescription]);
  24.            } else {
  25.                       [audioRecorder prepareToRecord];
  26.            }
  27. }

IOS_dummyDetailsNa linha 5, usamos uma atrocidade (stringByAppendingPathComponent) para criamos uma NSString para referenciar um arquivo .caf (Core Audio File, formato suportado pelo Mac OS X), logo depois, na linha 7, definimos nossas configurações de gravação (sobre os valores armazenados no NSDictionary ‘recordSettings’: não faço a mínima idéia do que significam estes valores.. apenas copiei). Na linha 18 criamos (alocamos na memória) um objeto AVAudioRecorder e utilizamos o método initWithURL para inicializar a instância e configurar os devidos parâmetros, repare que criamos um objeto NSError para acompanhar os resultados da configuração da instância e, caso nenhum erro ocorra, chamamos o método ‘prepareToRecord’.

E finalmente, para implementarmos o que foi declarado no header, vamos escrever o seguinte código na classe ‘PodcastAtTheBarViewController.m’ para controlar a ação de gravar:

  1. #import “PodcastAtTheBarViewController.h”
  2. @implementation PodcastAtTheBarViewController
  3. @synthesize recordButton;
  4. – (void)viewDidLoad
  5. { …
  6. – (IBAction)
  7. criarEntradaDeLogQuandoAlguemClicarNoBotao {…
  8. -(void)recordAudio {
  9.           if (!audioRecorder.recording)
  10.           {
  11.                     [audioRecorder record];
  12.                     [recordButton setTitle:@”Parar/Play” forState:UIControlStateNormal];
  13.           } else {
  14.                     [audioRecorder stop];
  15.                     [recordButton setTitle:@”Gravar” forState:UIControlStateNormal];
  16.                     audioPlayer = [[AVAudioPlayer alloc]
  17.                               initWithContentsOfURL:audioRecorder.url
  18.                               error:nil];
  19.                     audioPlayer.delegate = self;
  20.                     [audioPlayer play];
  21.           }
  22. }

Logo após a declaração da classe sintetizamos (criamos os getters e setters) das nossas propriedades com o @synthesize. Ignoramos os métodos que já foram implementados/explicados até chegar ao nosso novo método (-(void)recordAudio), na linha 8. Como a idéia é criar um exemplo simples, decidi utilizar um botão só para gravar, parar e reproduzir o áudio. Então, ao clicar no botão, os seguintes eventos irão ocorrer: Se não estiver gravando inicia a gravação com o método ‘record’ do nosso objeto audioRecorder e muda o label do botão para ‘Parar/Play’ quando no estado normal (esse é um daqueles métodos de nomes compostos, setTitle:forState:). Caso esteja gravando, o ‘audioRecorder’ será parado, o label vai mudar novamente para ‘Gravar’ e criaremos um novo objeto do tipo ‘AVAudioPlayer’, ele será iniciado apontando para o mesmo arquivo (NSURL) que o ‘audioRecorder’ e, como não estou preocupado com erros aqui, faço com que seja atribuído ao método ‘initWithContentsOfURL:error:’ o valor ‘nil’ (nulo).  A tarefa de reproduzir o áudio é delegada para o próprio objeto (linha 19) e finalmente ouvimos nossa gravação com o método ‘play’.

E se quisermos realizar alguma ação quando o ‘audioPlayer’ parar de reproduzir? Para ações como essa: temos os métodos delegados do nosso lado:

  1. -(void)audioPlayerDidFinishPlaying:
  2.           (AVAudioPlayer *)player successfully:(BOOL)flag {
  3.            NSLog(@”Terminou de reproduzir!”);
  4. }

Então é isso. Você deve estar pensando: “é muito fácil, vou colocar esta app na Apple Store e me preparar pra contar as verdinhas”. Não é bem assim, lembre-se que não estamos programando em Java, não temos um Garbage Collector nos ajudando a limpar os objetos que foram alocados em memória. Nesta pequena aplicação, por mais simples que seja, já existem trechos de código que causam memory leak. Para identificar estes possíveis ‘leaks’, utilizamos ferramentas de perfilamento, neste caso vamos usar a do próprio xcode. Inicie novamente a aplicação mas desta vez selecione ‘Profile’ ao invés de ‘Run’:

instruments

Na tela de opções de instrumentos, selecione o ‘Leaks’. Quando o ‘Instruments’ iniciar marque a opção ‘Leaks’ enquanto  o simulator inicia sua aplicação, comece a gravar e reproduzir sua voz, continue até que ele identifique o vazamento:

leaksPara corrigir isso? Sempre tenha em mente que, uma vez que algo é alocado em memória (alloc), terá que ser liberado em algum momento (release), então para corrigir o leak do ‘AVAudioPlayer’ utilizamos a seguinte abordagem (adapte as linhas 4 e 5 no código do método ‘recordAudio’):

  1. -(void)recordAudio {
  2. if (audioPlayer)
  3.             [audioPlayer release];
  4. audioPlayer = [[AVAudioPlayer alloc]
  5.                                initWithContentsOfURL:audioRecorder.url
  6.                                error:nil];

UPDATE: Talvez isso não seja mais necessário.

Então chegamos ao final de mais um post. Para quem ficou curioso sobre a tentativa frustrada de gravar um podcast, segue um trecho da gravação:

One thought on “Podcast no bar!Aprendendo Objective-C

  1. Realmente se preocupar com os releases não é tão necessario quando sair finalmente o iOS5 mas é bom que as pessoas que estejam começando agora com desenvolvimento iOS saibam a importancia do gerenciamento de memoria. Outro ponto é que para desenvolvimento para OS X não temos ARC e apesar de no OS X termos o GC ainda temos que se preocupar com o gerenciamento de memoria.

    Bom post
    []’s

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s