Um “Color Clock” nada mais é do que um relógio, cuja imagem de fundo se altera de acordo com a cor. O objetivo é simples e, no post de hoje, usaremos este problema para estruturar nosso código de forma a utilizar o padrão de delegação para reduzir o acoplamento das nossas unidades.
Problema
De acordo com o horário corrente do dispositivo, o app deve apresentar em uma única tela a hora atual no padrão HH:mm:ss, assim como sua representação hexadecimal #HHmmss. A cor de fundo deve mudar de acordo com sua representação hexadecimal e esta mudança acontece sempre que a hora corrente muda.
O padrão de delegação
A delegação é um padrão onde um objeto age em nome de, ou em coordenação com outro objeto. O objeto que delega, mantém a referência do objeto a ser delegado e no momento que for conveniente, envia uma mensagem (chamada de método). Essa mensagem tem o propósito de informar ao objeto delegado de que algum evento no objeto delegante está para acontecer, aconteceu ou vai acontecer.
No nosso caso, o objeto delegado é nosso ClockViewController e o delegante é DMClock. DMClock deve enviar mensagens a ClockViewController avisando a ele suas mudanças de estado e ClockViewController deve responder a esta mensagem manipulando e alterando a sua aparência de acordo.
Apesar de semelhante, não confunda com o padrão de notificação. Delegação é um padrão de 1:1, enquanto o padrão de notificação funciona em um modelo de broadcast, de 1:n. Ou seja, as mensagens do objeto delegante são escutadas somente por um objeto em ligação direta, enquanto notificações são escutadas por n outros objetos.
O relógio
Como o nosso problema indica que a cor de fundo devo mudar de acordo com a hora atual, é aparente que precisamos de algum tipo de timer. No nosso caso, a classe NSTimer serve como uma luva para o nosso problema, pois nos permite enviar mensagens a objetos de tempo em tempo, com um intervalo específico.
No nosso header, vamos então definir uma interface de delegação DMClockDelegate com um único método a ser implementado, -(void)clockDidUpdate
.
#import <Foundation/Foundation.h>; #import <UIKit/UIKit.h>; @protocol DMClockDelegate <NSObject>; - (void)clockDidUpdate; @end
A classe DMClock, que possui uma instância de NSTimer, acompanhará a mudança de tempo a cada segundo e delegará o que fazer com essa informação – neste caso – ao Controller.
@interface DMClock : NSObject @property (strong, nonatomic) NSTimer *timer; @property (nonatomic) float interval; @property (assign, nonatomic) id <DMClockDelegate> delegate; @property int hours; @property int minutes; @property int seconds; -(instancetype)initWithTimeInterval: (float)interval delegate: (id <DMClockDelegate>)delegate; -(void)start; -(void)stop; @end
Para receber as mensagens a cada segundo, nosso ViewController deve inicializar uma instância de DMClock utilizando um intervalo de 1.0 (segundo) e delegando a si mesmo a responsabilidade de fazer algo com as atualizações. Também expomos dois métodos públicos: start e stop, para que seja possível controlarmos a execução do relógio.
Obs.: Note que em Objective-C, id é o equivalente ao tipo genérico. E
(id <DMClockDelegate>)
lê-se como: Qualquer objeto que implementea interfaceo protocolo DMClockDelegate.
-(void)start { [self update]; self.timer = [NSTimer scheduledTimerWithTimeInterval: self.interval target: self selector: @selector(update) userInfo: nil repeats: YES]; }
Nosso método -(void)start
, inicializa um novo timer com o intervalo definido na inicialização de DMClock. Definimos em target que a mensagem disparada pelo timer deve ser enviada para a própria instância de DMClock. Em selector, definimos que o método a ser chamado é o método privado -(void)update
. Como queremos que o timer funcione até que o mesmo seja invalidado pelo método -(void)stop
, inicializamos com repeats: YES.
Para entendermos melhor, vale lembrar que em Objective-C, selectors funcionam como ponteiros de funções e podem ser utilizados de duas formas: Em tempo de compilação, através da diretiva de compilação @selector(nomeDoMetodo) ou em tempo de execução, utilizando a função NSSelectorFromString(@”nomeDoMetodo”).
Feito isso, já podemos inicializar um timer que chama nosso método update a cada segundo. Nossa função update então deve fica responsável por alterar os atributos hours, minutes e seconds para os valores correntes e avisar o self.delegate ViewController da mudança.
-(void)update { NSDate * date = [NSDate date]; self.hours = [self timePieceWithFormat:@"HH" date:date]; self.minutes = [self timePieceWithFormat:@"mm" date:date]; self.seconds = [self timePieceWithFormat:@"ss" date:date]; [self.delegate clockDidUpdate]; } -(int)timePieceWithFormat:(NSString *)format date:(NSDate *)date { NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setDateFormat: format]; return [[formatter stringFromDate: date] intValue]; }
ClockViewController
Nosso ClockViewController será composto de UILabels para representar cada uma das partes da nossa hora (lblHours, lblMinutes, lblSeconds) e uma UILabel para representar a data em hexadecimal (lblHexValue).
Obs.: Todas as ligações entre a View e as labels são feitas via storyboard
Como nosso controller pretende receber mensagens de DMClock, ele implementa DMClockDelegate.
#import <UIKit/UIKit.h> #import "DMClock.h" @interface ClockViewController : UIViewController <DMClockDelegate> @property (weak, nonatomic) IBOutlet UILabel *lblHours; @property (weak, nonatomic) IBOutlet UILabel *lblMinutes; @property (weak, nonatomic) IBOutlet UILabel *lblSeconds; @property (strong, nonatomic) IBOutletCollection(UILabel) NSArray *lblSeparators; @property (weak, nonatomic) IBOutlet UILabel *lblHexValue; @property (strong, nonatomic) DMClock * clock; @end
Agora precisamos “conectar” nosso ViewController com DMClock, para que o método -(void)clockDidUpdate seja chamado.
- (void)viewDidLoad { [super viewDidLoad]; self.clock = [[DMClock alloc] initWithTimeInterval: 1.0 delegate: self]; [self.clock start]; }
Objective-c é bem verboso, então, com um conhecimento básico de inglês, basta lermos a linha de inicialização de clock para entendermos o que está acontecendo e percebermos que se traduz para algo como:
“Aloque memória para DMClock e o inicialize com um intervalo de 1.0 segundos, delegando a responsabilidade para mim mesmo (ClockViewController)”
Como calcular o valor de uma hora ?
Parte do nosso problema é saber converter uma hora, por exemplo 13:35:16 em seu equivalente RGB (Red Green Blue). Para isso, usaremos a porção hora para representar a quantidade de cor vermelha, minuto para a cor verde, e segundos para a cor azul.
Os valores que cada um dos 3 campos pode assumir varia de #00 a #FF em hexadecimal ou, convertendo para o sistema decimal, de 0 a 255. Sabemos que as horas assumem valores de 0 a 24 e que minutos e segundos, de 0 a 60. Como os valores são diretamente proporcionais, para calcular o valor, basta fazermos uma regra de 3. Por exemplo:
24 horas -> 255 e 13 horas -> x
x = (13 * 255) / 2460 minutos -> 255 e 35 minutos -> x
x = (35 * 255) / 60
Podemos observar que a nossa função depende de dois valores. O valor a ser convertido (horas, minutos ou segundos) e o valor máximo que o valor a ser convertido pode assumir. De forma genérica, a função é:
hexValue = (value * MAX_COLOR_VALUE) / maxValue
#define MAX_COLOR_VALUE 255.0 // ... -(CGFloat)colorComponentFromInt:(int)value maxValue:(int)maxValue { return (MAX_COLOR_VALUE * value) / maxValue; }
Agora que temos como converter uma parte de uma hora, temos condições para gerar uma instância de UIColor, utilizando o método de classe colorWithRed: green: blue: alpha:.
[UIColor colorWithRed: [self colorComponentFromInt:self.clock.hours maxValue:24] green: [self colorComponentFromInt:self.clock.minutes maxValue:60] blue: [self colorComponentFromInt:self.clock.seconds maxValue:60] alpha: 1.0];
Como os valores de red, green e blue devem ser um float entre 0 e 1.0, precisamos modificar um pouco nosso método -(CGFloat)colorComponentFromInt:(int)value para garantir essa propriedade. Para isso, basta dividirmos o resultado final pela constante MAX_COLOR_VALUE.
// ... return ((MAX_COLOR_VALUE * value) / maxValue) / MAX_COLOR_VALUE; // ...
Agora, tudo que precisamos fazer é escutar a mensagem de DMClock, implementando o método -(void)clockDidUpdate no nosso ViewController.
-(void)clockDidUpdate { UIColor * color = [UIColor colorWithRed: [self colorComponentFromInt:self.clock.hours maxValue:24] green: [self colorComponentFromInt:self.clock.minutes maxValue:60] blue: [self colorComponentFromInt:self.clock.seconds maxValue:60] alpha: 1.0]; [self updateLabelsWithColor: color]; [self updateBackgroundWithColor: color]; }
Implementando o método -(void)updateLabelsWithColor:(UIColor *)color, usaremos os valores dos atributos da nossa instância de DMClock. (PODE SER MUDADO, talvez passado como parâmetro do método?)
-(NSString *)hexFromColor:(UIColor *)color { const CGFloat *components = CGColorGetComponents(color.CGColor); int red = round(components[0] * 255); int green = round(components[1] * 255); int blue = round(components[2] * 255); return [NSString stringWithFormat:"#%02x%02x%02x", red, green, blue]; } -(void)updateLabelsWithColor: (UIColor *) color { [self.lblHours setText: [NSString stringWithFormat:@"%02", self.clock.hours]]; [self.lblMinutes setText: [NSString stringWithFormat:@"%02i", self.clock.minutes]]; [self.lblSeconds setText: [NSString stringWithFormat:@"%02i", self.clock.seconds]]; [self.lblHexValue setText: [self hexFromColor:color]]; }
Para entendermos a implementação do método -(void)updateLabelsWithColor:(UIColor *)color, precisamos entender um pouco sobre formatação de strings. O formato @”%02i” significa que o valor deve ser interpretado como um número inteiro de 2 casas decimais, a ser completado com 0 a esquerda. O mesmo vale para @”#%02x”, mas para hexadecimais. Se tiver curiosidade, leia mais sobre formatação aqui.
Para transformarmos um ponteiro de UIColor para um NSString, definimos e implementamos o método -(NSString *)hexFromColor:(UIColor *)color, onde precisamos pegar cada um dos componentes da cor RGB e multiplica-los por 255 (lembra que os componentes são valores entre 0 e 1.0?).
Obs.: Em um projeto maior, a criação de uma Category para UIColor pode ser uma solução mais interessante e elegante.
A outra parte consiste em atualizar a cor de fundo da view de nosso ViewController. Para isso, definimos e implementaremos o método -(void)updateBackgroundWithColor:(UIColor *)color.
-(void)updateBackgroundWithColor: (UIColor *) color { [self.view setBackgroundColor: color]; }
Conclusão
No final do nosso projeto, o resultado deve ser semelhante a esse. A cor de fundo, as labels do relógio e a label referente ao valor hexadecimal atualizam a cada segundo e o horário é o mesmo que o do sistema operacional.
Esse projeto está disponível no github ou aqui em Swift. Fiquem a vontade para mandar pull requests =)
Ficou com alguma dúvida? Alguma parte poderia ser melhor explicada? Você implementou de uma outra forma ou em uma outra linguagem e gostaria de compartilhar? Deixe um comentário ou entre em contato direto comigo.