« Arrastrar archivos sobre el icono de una aplicación | Inicio | Entender el diseño web. »

Un clon de Photo Booth en 32 minutos (con un café a la mitad)

Una de las novedades de Leopard es la inclusión de un framework, ImageKit, dedicado a proveer al desarrollador de varias vistas dedicadas a la presentación de imágenes.

Como parte el framework, se incluyen dos vistas, IKImageBrowserView e IKImageView, dedicadas a la presentación, respectivamente, de colecciones de imágenes, y a una imagen de forma individual. Y por imagen, se entiende cualquier tipo soportado por QuickTime, lo que incluye vídeos y pdfs.

Para intentar ilustrar lo potente de estas nuevas APIs, vamos a hacer un ligero ejercicio de abstración.

El Reto

Hong Kong, 2007. Bruce Lee reta a El Gran Dragón, su discípulo más aventajado, a desarrollar un clon, con ciertas capacidades mermadas, de Photo Booth, la aplicación de Apple incluída con el sistema operativo. La merma de capacidades está, sobre todo, en la serialización del modelo a disco, o mejor dicho, en la falta del mismo. Vamos, que los datos no se guardan entre sesiones.

Clon 14 Funcionando

Clon 15 Funcionando

Así pues, El Gran Dragón va a necesitar de todos sus DeveloperSkillz y de la ayuda de algunas de las nuevas clases de Leopard, como IKPictureTaker, que expone y encapsula el proceso de captura de imágenes y vídeos utilizando la iSight incorporada en muchos Mac.

Continuemos el ejercicio de abstracción. El Gran Dragón y Bruce están en un ring, en un pabellón lleno hasta la bandera de público dispuesto a presenciar un espectáculo inolvidable...

17:55 El Gran Dragón se sienta ante la máquina. Va a ser una tarde muy, muy intensa.

17:56. El Gran Dragón crea un nuevo proyecto en XCode. En un alarde de sangre fría, elige como tipo de proyecto, una aplicación Cocoa (en Objective-C, por supuesto). Entre el público se escapan varios murmullos de sorpresa...

Clon 1

17.57. XCode crea la estructura del proyecto. El Gran Dragón, presa de la impaciencia, hace dobe click sobre MainMenu.nib para editarlo en el nuevo, brillante, y deliciosamente bonito Interface Builder.

17.58 El brillo de la nueva interfaz de Interface Builder no distrae a El Gran Dragón de su misión. Y precisamente, porque no se distrae, El Gran Dragón recuerda que debe linkar el framework Quartz a su proyecto, para que éste pueda compilar sin errores.

Clon 2 Add Framework

Por tanto, vuelve a XCode, presiona opción + comando + A (Project-> Add to Project) y navega hasta la ubicación de Quartz (/Sistema/Libreria/Frameworks/Quartz.framework), y da la orden, a la velocidad del rayo, de añadirlo al proyecto (sin copiarlo al mismo). Entre el público hay un par de amagos de desmayo.

18:00 Es el momento de comenzar a escribir código. Como El Gran Dragón hacer gala de su productividad y su rapidez con el teclado, crea directamente la clase del controlador de la aplicación en XCode (File_> New File -> Objective-C class) y la asigna el original nombre de PBController (Photo Bluff Controller). A la asingación del nombre le siguen ciertos murmullos de desaprobación entre el público. ¡Qué sabran esos!

18:01 El Gran Dragón añade dos outlets (uno para el listado de imágenes, llamado browser, y otro para la vista en detalle de cada una de las imágenes llamado view) a la cabecera de la clase. También añade un action. También importa Quartz De este modo, la cabecera (el fichero PBController.h) queda de la siguiente forma:

#import <Cocoa/Cocoa.h>
#import
<Quartz/Quartz.h>


@interface PBController : NSObject
{
IBOutlet IKImageBrowserView *browser;
IBOutlet IKImageView *view;
}

- (
IBAction)captureImage:(id)sender;

@end

18:02 EL Gran Dragón, con un movimiento felino, se pasa a Interface Builder. Añade un objeto al nib, y le asigna como nombre de clase, en el panel de Identity, PCController. Inmediatamente, Interface Builder parsea la cabecera y reconoce la existencia de los outlets y del action.

Clon 3 Object

18:03 Es el momento de trabajar en el interfaz. El Gran Dragón hace un clic en la instancia de NSWindow que ya está abierta en Interface Builder, y modifica uno de sus atributos en el panel de atributos, para hacerla Textured. ¿Por qué? Por que sí, El Gran Dragón no da explicaciones a nadie...

Clon 5 Textured

18:03 y un poquito más. ¡La hora Toolbar! Porque ahora se pueden diseñar los Toolbars directamente en Interface Builder. El Gran Dragón arrastra una instancia de NSToolbar sobre la ventana de su aplicación, y en la misma ventana presiona el botón de "Personalizar". Sobre la ventana de edición del Toolbar, arrastra una instancia de NSToolbarItem, y lo coloca en la posición más a la izquierda de todos los elementos del toolbar.. La sala estalla en aplausos. Pero aún o ha llegado lo mejor. El Gran Dragón hace control+drag desde el elemento que acaba de crear en el Toolbar hasta el controlador, y conecta el mismo con el action que creó al principio de su desafío: captureImage: A continuación, elimina los elementos sobrantes, hasta que su barra de herramientas queda como la de la imagen

Clon 7 Toolbarfinal

18:05. El Gran Dragón sabe que es el momento de arrastrar una instancia de IKImageBrowserView y colocarla en la ventana de su aplicación. Así lo hace, dejando espacio por la parte inferior para la instancia de IKIMageView. Tras arrastrar ambas vistas, su interfaz tiene este aspecto:

18:06 Hay que empezar a aproximar el interfaz a su aspecto final. Lo primero es añadir una barra de scroll para el browser. Así pues, entre suspiros de las damas, El Gran Dragón selecciona el browser y hace clic en Layout->Embed Objects In -> Scroll View, y casi en el mismo paso, edita las propiedades de la nueva ScrollView de scroll para eliminar la barra de scroll horizontal. Y, más aún, entre gritos de emoción de los caballeros, ajusta el tamaño del conjunto browser y barra de scroll para que ocupe todo el ancho de la ventana.

18:07 El Gran Dragón hace también la instancia de IKImageView del ancho de la ventana. Posteriormente selecciona las dos vistas, y las envuelve en un SplitView (Layout ->Embed Objects In -> Split View) También, en el panel de propiedades, ajusta el autosizing del Split View para vincularlo al tamaño de la ventana. El aspecto final de su interfaz es...

Clon 9 Interfaz Final 3

18:08 Aún falta asignar los outlets del controlador. Por eso El Gran Dragón lanza un par de latigazos de control+drag desde el controlador hasta, respectivamente, el browser y la vista de la imagen, finalizando la asignación. También, hace que el controlador de la aplicación sea el dataSource del browser.

18:09 El Gran Dragón se hace pis, así que hace una parada que aprovecha el público para ir al bar.

18:12 No ha estado mal. Es el momento de volver a XCode. Es el momento de empezar a implementar el controlador de la aplicación. Lo primero es añadir una property para guardar la colección de datos de la aplicación. También declara el getter y el setter de esa propiedad, así como un método llamadao addImageWithPath y dos de los métodos del protocolo que implementa el dataSource del browser. También, porque le gusta hacerlo así, modifica la declaración de los outlets, para asignarles el tipo adecuado. La cabecera, por tanto, quedará así:

#import <Cocoa/Cocoa.h>
#import
<Quartz/Quartz.h>


@interface PBController : NSObject
{
IBOutlet IKImageBrowserView *browser;
IBOutlet IKImageView *view;
NSMutableArray *list;
}

- (
IBAction)captureImage:(id)sender;


-(
NSMutableArray *) list;
-(
void) setList: (NSMutableArray *) aList;

-(
void) addImageWithPath: (NSString *) aPath;

-(
int) numberOfItemsInImageBrowser: (IKImageBrowserView *) aBrowser;
-(
id) imageBrowser: (IKImageBrowserView *) aBrowser
itemAtIndex: (
NSUInteger) index;
@end

18:14 Llega el momento para El Gran Dragón de empezar a implementar el controlador. Como le gusta empezar por el principio, primero añade el método de inicialización, donde inicializa (que por algo el método se llama como se llama) el array de datos:

-(id) init
{
self = [ super init ];
list = [ [ NSMutableArray alloc ] init ];

return self;
}

A continuación, pasa al método awakeFromNib, el que se ejecuta cuando se ha terminado de desplegar e inicializar todos los objetos incluídos en el nib. En ese método, El Gran Dragón invita amablemente al browser, ante el asombro del público, a que permita reordenar sus elementos, a que presente esas reordenaciones de forma animada, y a que su delegate sea el propio controlador:

-( void ) awakeFromNib
{
[
browser setAllowsReordering: YES ];
[
browser setAnimates: YES ];
[
browser setDelegate: self ];
}

18:16 Como va sobrado de tiempo, El Gran Dragón se pone a nevegar un rato por internet.

18:17 El Gran Dragón añade el getter y el setter de list:

-(
NSMutableArray *) list
{
return list;
}

-(
void) setList: (NSMutableArray *) aList
{
if( aList != list )
{
[
list release ];
list = [ aList retain ];
}

}

Y los dos métodos del dataSource:


-(
int)numberOfItemsInImageBrowser:(IKImageBrowserView *)aBrowser
{
return [ list count ];
}

-(
id) imageBrowser: (IKImageBrowserView *) aBrowser
itemAtIndex: (
NSUInteger) index
{
return [ list objectAtIndex: index ];
}

18:20 Hay que hacer limpieza antes de terminar...

-(
void) dealloc
{
[
list release ];

[
super dealloc ];
}

18:21 Es la hora de comenzar a programar en serio. Para que el IKImageBrowserView presente correctamente elementos en su interior, se le debe pasar, como valor de retorno de

-(
id) imageBrowser: (IKImageBrowserView *) aBrowser
itemAtIndex: (
NSUInteger) index

un objeto que implemente el protocolo IKImageBrowserItem. Eso implica que hay que crear una entidad que modele a cada una de las imágenes a presentar en el browser. En realidad no a la imagen en sí, sino que simplemente sería necesario encapsular el path de la imagen. El Gran Dragón, por tanto, pasa a modo "modelo".

18:21 y un poco más. El público está en completo silencio. El Gran Dragón escribe la cabecera de su entidad:

#import <Cocoa/Cocoa.h>
#import
<Quartz/Quartz.h>

@interface DataSourceItem : NSObject
{
NSString* path;
}

-(
void)setPath:(NSString*)inPath;
-(
NSString * ) path;
@end

En la implementación de la clase, escribe el getter y el setter del path:

- (
void)setPath:(NSString*)inPath
{
if (path != inPath)
{
[
path release];
path = [inPath retain];
}
}
-(
NSString *) path
{
return path;
}

Y los tres métodos a los que le obliga el protocolo IKImageBrowserItem

- (
NSString*)imageRepresentationType
{
return IKImageBrowserPathRepresentationType;
}

- (
id)imageRepresentation
{
return path;
}

- (
NSString*)imageUID
{
return path;
}

18:22 El Gran Dragón hace un alto para reflexionar. Al devolver como imageRepresentatonType de esta entidad la constante IKImageBrowserPathRepresentationType le está diciendo a quien quiera consumir su interfaz que lo que le van a devolver como imageRepresentation va a ser el path del fichero correspondiente. COmo así ocurre, por cierto. ¿Qué otras cosas se pueden devolver? Pues una representación tiff, un quicktime, un pdf...

18:23 Se acabó la reflexión. Es el momento de implementar le método que se va a ejecutar cuando se haga clic en el item del toolbar: captureImage. Ese método obtendrá una referencia a IKPictureTaker (que, como cabe esperar es un singleton), y setea una serie de parámetros iniciales del mismo (el área de cropping, la imagen inicial, y si se debe o no mostrar el botón de "elegir archivos" y si se deja o no aplicar efectos a la imagen capturada).

También, se da la orden de lanzar el PictureTaker, pasando como parámetro el selector que se quiere ejecutar al finaizar la captura.

- (
IBAction)captureImage:(id)sender
{
IKPictureTaker *pictureTaker = [IKPictureTaker pictureTaker];

[pictureTaker
setInputImage: [ [ [ NSImage alloc] initByReferencingFile:@"/Library/Desktop Pictures/Black & White/Sea Mist.jpg" ] autorelease]];
[pictureTaker
setValue: [ NSValue valueWithSize:NSMakeSize(300, 200)] forKey:IKPictureTakerCropAreaSizeKey ];
[pictureTaker
setValue: [ NSNumber numberWithBool: NO ] forKey:IKPictureTakerAllowsFileChoosingKey ];
[pictureTaker
setValue: [ NSNumber numberWithBool:YES ] forKey:IKPictureTakerShowEffectsKey ];

[pictureTaker
beginPictureTakerWithDelegate:self didEndSelector:@selector(pictureTakerValidated:code:contextInfo:) contextInfo:nil];
}

18:25 Como El Gran Dragón se ha gustado con lo del singleton, y sigue estando sobrado, se va a tomar un café. Con leche. Sin azúcar.

18:26 El Gran Dragón comienza con la implementación del método que se ejecutará cuando se cierre el panel. Ese método comprobará si el botón que se ha clickado en el panel para cerrarlo ha sido el de OK, y en ese caso, obtendrá una referencia a la imagen capturada, creará una cadena de texto con el path de la imagen a escribir (con un autocontador embebido en el mismo), y llamará a otro método del controlador, encargado de crear la instancia de DataSourceItem y añadirla al modelo de la aplicación. Así pues:

- (
void) pictureTakerValidated:(IKPictureTaker*) pictureTaker code:(int) returnCode contextInfo:(void*) ctxInf
{
if(returnCode == NSOKButton){
NSImage *outputImage = [ pictureTaker outputImage ];

NSString *outputPath = [ [ NSString stringWithFormat: @"~/Pictures/cameracapturer-snap-%i.tiff", [ list count ] ] stringByExpandingTildeInPath ];
[ [ outputImage TIFFRepresentation ]
writeToFile: outputPath atomically:YES ];

[
self addImageWithPath: outputPath ];

}
else{
//cancelado por el usuario, no hacer nada
}
}

18:27 El paso siguiente es obvio: debe crearse el método que añade la imagen al modelo, y mandar recargar el browser:
-(
void) addImageWithPath: (NSString *) aPath
{
DataSourceItem *item = [ [ DataSourceItem alloc ] init ];
[ item
setPath: aPath ];

[
list addObject: item ];

[ item release ];

[
browser reloadData ];
}

En este momento, El Gran Dragón compila la aplicación. Ante el asombro general, hace clic en el botón del toolbar y se abre el panel de captura. Oooooooooooooooooohs y aaaaaaaaaaaaaaaaaaahhhhhs acompañan a la primera imagen que El Gran Dragón captura de sí mismo:

Clon 15 Funcionando-1


imagen que aparece en el browser como por arte de magia.

18:29 El Gran Dragón abandona su puesto para ayudar a los sanitarios que han entrado en la sala a ayudar a una señora que se ha desmayado ante tanta tensión. A continuación, implementa otro método del delegate del browser, el que se ejecuta cuando se ha seleccionado alguno de los componentes del mismo, apra ofrecer una vista ampliada de la imagen en el IKImageVIew. El método es:

-(
void) imageBrowserSelectionDidChange:(IKImageBrowserView *) aBrowser
{
NSIndexSet *actualSelection = [ browser selectionIndexes ];
if( [ actualSelection count ] == 1 )
{
int index = [ actualSelection firstIndex ];
NSString *path = [ [ list objectAtIndex: index ] path ];
NSURL *imageURL = [ NSURL fileURLWithPath: path ];
//NSLog( @"setting la vista grande %@", imageURL );
[
view setImageWithURL: imageURL ];
}
}

Como puede verse, la cosa es sencilla. Si sólo se ha seleccionado una imagen, se obtiene el valor del path de la misma, que se pasa a la vista de la imagen. El resultado...

Clon 12 Funcionando

IKImageView implementa un HUD en el que se pueden modificar bastantes parámetros de la imagen (exposición, contraste) e incluso aplicarla efectos. Todo, directamente para su disfrute nada más sacarla de la caja.

18:31 Ya sólo falta el último paso: implementar la reordenación de las imágenes del browser. Tampoco tiene tanto misterio, se trata de implementar el método

- (
BOOL) imageBrowser:(IKImageBrowserView *) aBrowser
moveItemsAtIndexes: (
NSIndexSet *)indexes
toIndex:(
NSUInteger)destinationIndex

del protocolo del dataSource. Lo que hay que hacer es obtener el set con los índices de los elementos seleccionados, así como el índice donde se van a soltar. Luego, hay que crear un array temporal en el que colocarán los elementos arrastrados, mientras se eliminan del original. Posteriormente, ese array se inserta en la posición en la que se soltaron las imágenes, y se recarga el browser:

- (
BOOL) imageBrowser:(IKImageBrowserView *) aBrowser
moveItemsAtIndexes: (
NSIndexSet *)indexes
toIndex:(
NSUInteger)destinationIndex
{
NSInteger index;
NSMutableArray* tempArray;

tempArray = [ [ [
NSMutableArray alloc ] init ] autorelease ];

for(index = [ indexes lastIndex ]; index != NSNotFound; index = [ indexes indexLessThanIndex:index ] )
{
if (index < destinationIndex)
destinationIndex --;

id obj = [ list objectAtIndex:index ];
[ tempArray
addObject:obj ];
[
list removeObjectAtIndex:index ];
}

// Then insert the removed items at the appropriate location.
NSInteger n = [ tempArray count ];
for( index = 0; index < n; index++ )
{
[
list insertObject: [ tempArray objectAtIndex: index ] atIndex: destinationIndex ];
}

return YES;
}


18:32 El ruido en la sala es ensordecedor. La multidud, enardecida, asalta el ring y se lleva a El Gran Dragón a hombros, como si fuera un torero. Acaba de nacer una leyenda...

El proyecto completo se puede descargar de su repositorio:

svn co http://svn.liadorasoft.com/photobluff Photo_Bluff

Comentarios

Congrats!! muy buen tutorial, oh Gran Dragón!!!
Espero poder leer muchos mas tutoriales para poder aprender las técnicas del maestro kung-fu!!
si me permites una sugerencia... a ver si sabes como aplicar filtros de Core Image (blur, edges,...) a una ventana OpenGL fullscreen (inicializada con glut o agl... sin ide, vamos :P).

felicidades!!!

Gracias, pequeño saltamontes.

El Gran Dragón intentará primero entender a qué hace referencia tu pregunta, y posteriormente buscará una solución en su interior.

Lo intentaré cuendo tenga un ratillo :) Muchas gracias!

Un excelente ejercicio, bastante claro y eso que soy un switcher reciente, y apenas si conozco xcode...

Se podra hacer algo parecido en python?

Publicar un comentario

(Si no dejó aquí ningún comentario anteriormente, quizás necesite aprobación por parte del dueño del sitio, antes de que el comentario aparezca. Hasta entonces, no se mostrará en la entrada. Gracias por su paciencia).