Gran Maestro: La prueba de hoy en el largo y tortuoso camino del conocimiento, Gran Dragón, es programar una vista clickable, que puedas reutilizar en todas las futuras pruebas con las que te vas a encontrar, y que sea capaz de notificar al controlador de la aplicación en la que se esté utilizando cada uno de los clicks que reciba.
Gran Dragón: Gracias, Gran Maestro, por proponerme un reto de tal importancia.
El Gran Dragón sabe que no puede traicionar la confianza ni las expectatias de su maestro. Sabe que ha comenzado un largo camino, un camino lleno de escarpadas montañas, cruzado por anchos y profundos ríos, un camino del que parten otros muchos caminos que le pueden distraer de su objetivo final: llegar al estado mental en el que sólo hay una forma de pensar; la que termina con la máxima cohesión y el mínimo acoplamiento.
Por eso, y porque El Gran Dragón cree que la mejor forma de programar es iterativa, produciendo desde la primera iteración algo funcional, aunque no esté necesariamente muy refinado, comienza su aventura declarando una subclase de NSView, que, simplemente, sea capaz de capturar los clicks de ratón que se produzcan sobre ella. Además, El Gran Dragón quiere proporcionar feedback visual de las interacciones con su vista, por lo que va a hacer que la vista cambie de color, del gris en reposo, a un gris más claro cuando sea clickada.
Por tanto, la cabecera de su vista será:
#import
@interface DNClickableView : NSView
{
BOOL mouseIsPressingMe;
}
@end
Al inicializar la instancia de su clase, el Gran Dragón hará que la variable booleana valga false. Cada vez que redibuje el contenido de su vista, comprobará el valor de esa variable: si es true, el fondo será gris claro, y si es false, gris:
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
mouseIsPressingMe = NO;
}
return self;
}
- (void)drawRect:(NSRect)rect
{
if ( mouseIsPressingMe )
{
[ [ NSColor lightGrayColor ] set ];
}
else
{
[ [ NSColor grayColor ] set ];
}
NSRectFill( rect );
}
Para capturar los eventos de ratón sobre esa vista, basta con declararlos, sobreescribiendo por tanto los correspondientes a NSView, en la implementación de la subclase:
- ( void ) mouseDown: ( NSEvent * ) theEvent
{
mouseIsPressingMe = YES;
[ self setNeedsDisplay: YES ];
}
- (void)mouseUp:(NSEvent *)theEvent
{
mouseIsPressingMe = NO;
[ self setNeedsDisplay: YES ];
}
Por tanto, la declaración completa de la implementación de la clase sería:
#import "DNClickableView.h"
@implementation DNClickableView
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
mouseIsPressingMe = NO;
}
return self;
}
- (void)drawRect:(NSRect)rect
{
if ( mouseIsPressingMe )
{
[ [ NSColor lightGrayColor ] set ];
}
else
{
[ [ NSColor grayColor ] set ];
}
NSRectFill( rect );
}
#pragma mark -
#pragma mark mouse handling
- ( void ) mouseDown: ( NSEvent * ) theEvent
{
mouseIsPressingMe = YES;
[ self setNeedsDisplay: YES ];
}
- (void)mouseUp:(NSEvent *)theEvent
{
mouseIsPressingMe = NO;
[ self setNeedsDisplay: YES ];
}
@end
El Gran Dragón sabe que debe testar mucho y desde muy pronto, por lo que, para probar la funcionalidad de su clase, declara un controlador con un outlet hacia su vista, y compila y prueba el proyecto (El Gran Dragón da por supuesto que le lector sabe cómo hacer eso en XCode 3; no obstante, se puede encontrar más información sobre cómo hacerlo aquí)
Bien, el Gran Dragón está satisfecho de su primera iteración. Tiene una aplicación que compila aunque no cumple con uno de los requisitos pedidos: que la vista notifique al controlador los clicks recibidos.
Ahora ha llegado el momento de meditar, con la cabeza fría, sobre los diferentes caminos que se presentan ante el Gran Dragón.
El primer camino, el más obvio, y probablemente el más sencillo y rápido de implementar sería declarar una variable de clase en la vista, en la que guardar una referencia al controlador, de forma que, al capturar el clic, se envíe un mensaje al controlador de la aplciación.
No está mal, piensa El Gran Dragón. Sin embargo, ¿hasta qué punto esa solución es portable? ¿Hasta qué punto esa misma vista se podría reutilizar en más proyectos?
El Gran Dragón sonríe, con la satisfacción de saberse en el buen camino. Sabe que esa primera solución, la más rápida, es, probablemente, la menos adecuada. En primer lugar, porque implica que tanto la vista como el controlador tengan una referencia cruzada el uno al otro.
En segundo lugar, porque si se hacen las cosas como se debería, la referencia al controlador que se guarda en la vista debería estar declarada con el tipo del controlador. Lo que no la hace precisamente portable. Claro, que eso se podría solventar haciendo que esa referencia sea de tipo un protocolo que implemente el controlador. Lo que tampoco es bastante portable, porque obliga a que el controlador de cualquier aplicación que quiera utilizar esta vista deba implementar ese protocolo.
El Gran Dragón, por tanto, deshecha la primera solución.
La segunda solución que le viene a la cabeza es utilizar una notificación, de forma que la vista lance esa notificación (lo que en la mayoría de lenguajes se llama un evento), que será escuchada por el controlador.
El problema en esta segunda solución es que la implementación de las notificaciones en Objective-C es un poco particular. Las notificaciones se emiten y se reciben a través de un singleton (NSNotificationCenter) de forma que es relativamente sencillo pisarse un pie con el otro, y hacer que una notificación sea escuchada por quien no debe.
Además, las notificaciones hacen el código un poco más difícil de leer, y tampoco es que sean demasiado amigables a la hora de pasar parámetros.
El Gran Dragóm, por tanto, inmerso como está en la búsqueda de la elegancia en su código, deshecha la segunda solución.
La tercera solución, sin embargo, es utilizar uno de los patrones más comunes en Cocoa: la delegación.
La extensión por delegación no es más que una forma de composición un poco más sofisticada. Una clase, la que se quiere extender, puede delegar detalles concretos de implementación (lo que es susceptible de cambiar entre una utilización y otra de la clase) en otras clases, de forma que, en tiempo de ejecución, se pueden delegar responsabilidades en clases concretas declaradas a ese efecto.
Ese patrón es muy sencillo de implementar en Objective-C, como bien sabe El Gran Dragón, gracias a la naturaleza tan dinámica del lenguaje, y a que siempre se puede saber si una clase va a poder responder o no a un mensaje.
El Gran Dragón comienza, por tanto, la escalada de la delegación declarando una variable de clase en la vista, junto con sus accesors:
#import
@interface DNClickableView : NSView
{
BOOL mouseIsPressingMe;
id delegate;
}
- (id) delegate;
- (void) setDelegate: (id) newDelegate;
-(void) viewWasClicked;
@end
Además, declara un método que será ejecutado cuando se haga click en la vista (viewWasClicked)
Por tanto, los métodos dedicados a la captura de los clics quedarían así:
- ( void ) mouseDown: ( NSEvent * ) theEvent
{
mouseIsPressingMe = YES;
[ self setNeedsDisplay: YES ];
}
- (void)mouseUp:(NSEvent *)theEvent
{
mouseIsPressingMe = NO;
[ self setNeedsDisplay: YES ];
[ self viewWasClicked ];
}
Los acessors para el delegate:
- (id)delegate
{
return delegate;
}
- (void)setDelegate:(id)newDelegate
{
delegate = newDelegate;
}
El método que gestiona los clicks:
-(void) viewWasClicked
{
if( [ delegate respondsToSelector: @selector( viewWasClicked ) ] )
{
[ delegate viewWasClicked ];
}
}
Ahí es donde el Gran Dragón se siente más orgulloso. Para evitar estar enviando mensajes a nil, comprueba que lo que se haya asignado como delegate responde al mensaje viewWasClicked. En caso de responder, reenvía el mensaje a esa clase, y en caso de no responder a él, lo deja morir.
En el controlador, sólo faltaría asignar el delegate:
-(void) awakeFromNib
{
[ clickView setDelegate: self ];
}
Por tanto, la implementación completa de la vista sería:
#import "DNClickableView.h"
@implementation DNClickableView
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
// Initialization code here.
mouseIsPressingMe = NO;
}
return self;
}
- (void)drawRect:(NSRect)rect
{
if ( mouseIsPressingMe )
{
[ [ NSColor lightGrayColor ] set ];
}
else
{
[ [ NSColor grayColor ] set ];
}
NSRectFill( rect );
}
#pragma mark -
#pragma mark mouse handling
- ( void ) mouseDown: ( NSEvent * ) theEvent
{
mouseIsPressingMe = YES;
[ self setNeedsDisplay: YES ];
}
- (void)mouseUp:(NSEvent *)theEvent
{
mouseIsPressingMe = NO;
[ self setNeedsDisplay: YES ];
[ self viewWasClicked ];
}
#pragma mark -
#pragma mark delegate handling
- (id)delegate
{
return delegate;
}
- (void)setDelegate:(id)newDelegate
{
delegate = newDelegate;
}
-(void) viewWasClicked
{
if( [ delegate respondsToSelector: @selector( viewWasClicked ) ] )
{
[ delegate viewWasClicked ];
}
}
-(void) dealloc
{
[ delegate release ];
[ super dealloc ];
}
@end
y la del controlador:
@implementation DNAppController
-(void) awakeFromNib
{
[ clickView setDelegate: self ];
}
#pragma mark -
#pragma mark DNClickableView delegate method
-(void) viewWasClicked
{
NSLog( @"DNAppController.viewWasClicked" );
}
@end
Mucho mejor. El Gran Dragón se siente satisfecho, porque aunque aparentemente haya implementado una solución similar a la primera que deshechó, ha ganado muchísimo en flexibilidad. En este ejemplo, el delegate se ha asignado al controlador, pero podría ser cualquier clase, instanciada directamente en el controlador, o producto de algún patrón de creación.
Sin embargo, el Gran Dragón sabe que su Gran Maestro, hombre noble y de infinita sabiduría pero muy exigente, no va a ver con buenos ojos que haya declarado la referencia al delegate como id, sin más.
Objective-C soporta protocolos, por lo que el método que se va a delegar, podría estar declarado en un protocolo, en un protocolo informal en realidad. ¿Porqué informal? Porque de esa forma no se obliga a nadie que lo cumpla a que implemente todos los métodos declarados en el mismo.
Por cierto, El Gran Dragón sabe que su solución está un pelín obsoleta con la introducción de Objective-C 2.0, ya que los ingenieros de Apple recomiendan que en la nueva iteración del lenguaje no se utilicen protocolos informales, sino que prefieren que se utilicen protocolos formales con métodos opcionales.
No obstante, El Gran Dragón quiere terminar con el proyecto, porque fuera del templo brilla el sol y le apetece salir a hacer fotos, así que comienza por declarar el protocolo informal, en la cabecera de la vista:
@interface DNClickableView : NSView
{
BOOL mouseIsPressingMe;
id delegate;
}
- (id) delegate;
- (void) setDelegate: (id) newDelegate;
@end
@interface NSObject ( DNClickableViewDelegate )
-(void) viewWasClicked;
@end
De esa forma, el compilador creerá que todos los objetos conforman ese protocolo, aunque en realidad, al ser informal, no tienen porqué implementar el método declarado en él.
El Gran Dragón está contento. Su subclase de NSView puede reutilizarse en todos los proyectos, porque no está vinculada a ningún controlador concreto, ni siquiera a un interfaz de éste, y lo que es mejor, la funcionalidad que se quiere asignar al click del ratón, que puede ser diferente en cada caso, puede estar encapsulada en una clase que se le proporciona a la vista, sin que ésta sepa nada de ella, ni de sus detalles de implementación, ni de su forma de creación.
El Gran Maestro, mientras tanto, mira orgulloso a su pupilo, pensando ya en el próximo reto...