What color is it? Construindo um “Color Clock” com Objective-C e Swift

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 implemente a interface o 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 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

ClockViewControllerscreen-shot-2016-09-07-at-01-46-06

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) / 24

60 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

Captura de tela de ClockViewController

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.

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s