viernes, 20 de enero de 2012

Nuevo blog: GeneXus for Smart Devices

Esta nota es solo para contarles que estamos empezando un nuevo blog: GeneXus for Smart Devices.

La idea es tener un lugar donde contar las cosas que estamos haciendo, nuevas funcionalidades, tips, como extender los generadores, etc.

Los invito a que lo visiten y se suscriban (por RSS). Ya tiene un par de notas, la bienvenida y una nota contando una funcionalidad nueva :)

El blog está en inglés, porque parece lo más razonable para una comunidad de desarrolladores.

jueves, 12 de enero de 2012

Implementar un User Control para el generador iOS

En los generadores para Smart Devices en GeneXus X Evolution 2, tenemos la posibilidad de crear user controls, para extender el comportamiento del generador.

La idea de esta nota es contar como desarrollar un nuevo user control para el generador iOS.

Nota: crear un nuevo user control involucra varios pasos: implementación en la plataforma, definición del control, implementar resolvers para las propiedades, distribución, etc. Ahora me voy a concentrar solo en la parte de implementación.

Como ejemplo, voy a mostrar como implementar el SD ImageMap. Básicamente lo que hace es mostrar una imagen de fondo con un conjunto de imágenes en posiciones determinadas, que cuando se seleccionan tiene la posibilidad de disparar una acción.

El código completo del control (al día de hoy) lo pueden ver en https://gist.github.com/1600064

Clase base

Como decía, el control muestra una lista de imágenes con una imagen grande de fondo. Esa lista puede venir de una tabla en la base de datos, o de un SDT collection. En cualquier caso, los controles que reciben una lista de registros, se implementan a partir de un control Grid en GeneXus, cambiando la propiedad Control Type según corresponda.

Lo primero que tenemos que hacer entonces es crear la clase GXControlImageMap, como subclase de GXControlGridBase.

Advertencia: los nombres que empiezan con "GX" están reservados. En este caso el control es parte del Framework, por lo tanto puede llamarse así. Cuando implementen sus controles usen algún otro nombre...

Creación de la vista

Lo primero que debemos implementar es el método que crea la vista que va a utilizar el control. La forma de hacerlo es implementar el método newGridViewWithFrame:, que será llamado por la clase base cuando se haga el loadView.

Es importante notar que no es aconsejable implementar el loadView directamente, ya que la clase base hace varias cosas además de crear la vista del control (por ejemplo crea otra vista donde coloca la vista que estamos creando, le aplica la propiedad "Visible", etc.).

- (UIView *)newGridViewWithFrame:(CGRect)frame {
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:frame];
    [imageView setAutoresizingMask:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth];

    NSString *imageName = [self imageName];
    if (imageName) {
        UIImage *embededImage = [GXResources imageForName:imageName];
        if (embededImage) {
            [imageView setImage:embededImage];
        }
        else {
            NSURL *imageUrl = [GXResources urlForImageName:imageName];
            [imageView setImageWithURL:imageUrl placeholderImage:nil];
        }
    }
    
    [imageView setUserInteractionEnabled:YES];
    
    UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleImageTap:)];
    
    [imageView addGestureRecognizer:tapGestureRecognizer];
    
    [tapGestureRecognizer release];
    
    return imageView;
}

El método newGridViewWIthFrame: hace lo siguiente:
  • crea un UIImageView donde va a dibujar la imagen de fondo
  • carga la imagen, el valor lo obtiene usando el método imageName (ver más adelante)
  • agrega un "tap gesture recognizer" para poder detectar cuando se hace un tap sobre alguna de las imágenes
Acceso a la vista del control

El UIImageView que estamos creando en este método queda accesible usando el método gridView de la clase base. Es recomendable implementar un método que permita acceder a esta vista con el tipo de datos correcto, ya el gridView es un UIView genérico. Por lo tanto, el control implementa el método imageView

- (UIImageView *)imageView {
    return (UIImageView *)[self gridView];
}

Propiedades definidas en GeneXus

El control también va a necesitar acceso a las propiedades definidas en GeneXus. Para eso define nueve métodos, uno por cada una de las propiedades:

- (NSString *)imageName;
- (NSString *)imageAtt;
- (NSString *)imageField;
- (NSString *)horizontalCoordinateAtt;
- (NSString *)horizontalCoordinateField;
- (NSString *)verticalCoordinateAtt;
- (NSString *)verticalCoordinateField;
- (NSString *)sizeAtt;
- (NSString *)sizeField;

La propiedad imageName tiene el nombre de la imagen de fondo a usar.

Las demás propiedades vienen en pares, una con sufijo "Att" y la otra con sufijo "Field". La propiedad con sufijo "Att" indica el nombre del atributo o variable que contiene el valor. La propiedad con sufijo "Field" contiene el "field specifier", que se usa en el caso que el "Att" sea un SDT, e indica el nombre del miembro del SDT.

Ejemplos:
  • Si usamos el atributo MyAttribute, en la propiedad "Att" viene "MyAttribute" y la propiedad "Field" viene vacía
  • Si usamos la variable &MyVar, en la propiedad "Att" viene "&MyVar" y la propiedad "Field" viene vacía.
  • Si usamos un miembro de un SDT, &MySDT.Item, entonces en la propiedad "Att" viene "&MySDT" y en la propiedad "Field" viene "Item"
Los cuatro grupos de propiedades que tienen esta característica son:
  • image: contiene la imagen del ítem a mostrar
  • horizontalCoordinate: la posición horizontal donde se debe mostrar, relativo al tamaño de la imagen de fondo (el origen de coordenadas {0,0} está en la parte superior izquierda de la imagen de fondo)
  • verticalCoordinate: la posición vertical
  • size: el tamaño con el que se debe mostrar la imagen, relativo al tamaño de la imagen de fondo.
Las propiedades se leen de forma muy simple, por ejemplo:

- (NSString *)horizontalCoordinateAtt {
    return [[self properties] getPropertyValueString:@"@SDImageMapHCoordAtt"];    
}

La única que tiene algo más de complejidad es imageName, que por ser una imagen viene con un prefijo que es el GUID de las imágenes, y debemos separarlo para poder usarla:

- (NSString *)imageName {
    NSString *imageName = [[self properties] getPropertyValueString:@"@SDImageMapImage"];
    imageName = [GXObjectHelper parseObjectNameOfType:kGXObjectIdImage from:imageName];
    return imageName;
}

Carga de datos

La carga de datos la maneja la clase base, y cuando termina de obtener los datos del servidor le manda el mensaje reloadData al control.

En este caso la implementación invoca un método privado loadData: pasándole la cantidad de registros que tiene el proveedor de datos:

- (void)reloadData {
    [self loadData:[[self entityDataListProvider] numberOfLoadedEntitiesInSection:0]];
}

El método loadData: es más interesante.

Lo primero que hace es borrar cualquier imagen que tuviera ya cargada, porque las va a volver a agregar:

[[[self imageView] subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];

Luego obtiene los valores de las propiedades configuradas en GeneXus:

NSString *imageNameAtt = [self imageAtt];
NSString *imageNameField = [self imageField];
NSString *hCoordAtt = [self horizontalCoordinateAtt];
NSString *hCoordField = [self horizontalCoordinateField];
NSString *vCoordAtt = [self verticalCoordinateAtt];
NSString *vCoordField = [self verticalCoordinateField];
NSString *sizeAtt = [self sizeAtt];
NSString *sizeField = [self sizeField];

Lo siguiente es iterar sobre los registros disponibles (la cantidad se le pasa como parámetro al loadData: en la variable count)

for(NSUInteger index = 0; index < count; index++)

y para cada uno, obtener los valores. Para eso usamos el método valueForEntityDataFieldName:fieldSpecifier:indexPath: de la clase base:

NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
NSString *imageName = [self valueForEntityDataFieldName:imageNameAtt fieldSpecifier:imageNameField indexPath:indexPath];
NSNumber *hCoord = [self valueForEntityDataFieldName:hCoordAtt fieldSpecifier:hCoordField indexPath:indexPath];
NSNumber *vCoord = [self valueForEntityDataFieldName:vCoordAtt fieldSpecifier:vCoordField indexPath:indexPath];
NSNumber *size = [self valueForEntityDataFieldName:sizeAtt fieldSpecifier:sizeField indexPath:indexPath];

Nótese que el indexPath le indica al proveedor de datos cual es el registro que estamos procesando.

El resto del código del evento es específico al control y no viene mucho al caso, lo pueden ver en el link con el código completo. Lo único para destacar es que con los valores de las propiedades leídas crea un UIImageView para cada ítem y lo agrega a la vista principal del control con

[[self imageView] addSubview:itemView];

Disparo de acciones

Por último, lo que hace el control es disparar la "default action" cuando se selecciona una de las imágenes.

Eso se configuró en el método newGridViewWithFrame: cuando agregamos el "tap gesture recognizer", que va a invocar el método handleImageTap:

- (void)handleImageTap:(UITapGestureRecognizer *)sender {
    UIView *mainView = [self imageView];
    
    CGPoint tapPoint = [sender locationInView:mainView];
    UIView *tappedView = [mainView hitTest:tapPoint withEvent:nil];
    
    if (tappedView && tappedView != mainView) {
        NSUInteger index = [tappedView tag];
        [self executeDefaultActionForEntityAtSection:0 row:index];
    }
}

Este método determina sobre cual ítem se hizo el tap, y luego simplemente llama al método executeDefaultActionForEntityAtSection:row: de la clase base para que se encargue.

Resumen

La nota quedó bastante larga, pero en realidad no es mucho lo que hay que hacer para implementar un user control. La parte más compleja se encarga la clase base: obtener los datos (ya sea de un data provider o de un SDT), lectura de propiedades, manejo de la vista del control, manejo de eventos, etc.

En resumen, lo que tiene que hacer el control es:

  • crear la vista, en el método newGridViewWithFrame:
  • implementar el método reloadData para cargar los datos que vienen en el provider (en este caso se implementó en un método privado loadData:, pero es lo mismo)
  • determinar cuándo se debe disparar una acción y cuál es el ítem que corresponde.

jueves, 5 de enero de 2012

Objective-C: implementar método de forma opcional en runtime

Una de las ventajas que tiene Objective-C, es que es un lenguaje dinámico.

Eso permite tener protocolos (interfaces en otros lenguajes...) con métodos declarados como opcionales, y preguntar en tiempo de ejecución si la clase que voy a usar implementa el método.

Esta característica está buena, pero ahora necesito ir un paso más lejos. Quiero que una clase implemente o no un método, pero según alguna condición que solo conozco en tiempo de ejecución.

El caso de uso es el siguiente. Cuando se tiene una tabla (UITableView) se pueden mostrar los registros agrupados en secciones, y en esos casos se puede cambiar la apariencia de lo títulos de las secciones.

En GeneXus tenemos la posibilidad de cambiar estos títulos usando la clase del tema GroupSeparator (es nueva, la estamos implementando...)

El problema es que esa propiedad puede estar vacía, y en ese caso lo que quiero es que la tabla muestre el separador por defecto, que tiene un fondo con gradiente, letra con sombra, etc.

Entonces lo que quiero es que el control Grid implemente el método tableView:viewForHeaderInSection: solamente si tiene tema y si el tema tiene la propiedad "Group Separator".

Si bien en el lenguaje es natural tener métodos opcionales, no es evidente como hacer para que una clase implemente un método a veces sí y a veces no...

La solución que encontré creo que es bastante buena... y consiste en implementar el método respondsToSelector:, y devolver lo que corresponda. Para el resto de los métodos, podemos dejar que el lenguaje resuelva de forma automática.

La implementación del método respondsToSelector: queda entonces así:
- (BOOL)respondsToSelector:(SEL)aSelector {
    if (aSelector == @selector(tableView:viewForHeaderInSection:)) {
        return [[self themeClassList] groupSeparatorThemeClass] != nil;
    }
    return [super respondsToSelector:aSelector];
}


Hay un detalle con esta implementación. La tabla le pregunta al delegate si implementa el método solo una vez, y se guarda el resultado. Por lo tanto, cuando cambia alguna de las condiciones (en este caso, cuando se le aplica el tema al control) hay que poner el delegate en nil y volver a asignarlo para que vuelva a preguntar si implementa el método.

Otra opción puede ser tener una clase proxy que tenga una lista de selectors que se tienen que ignorar en tiempo de ejecución, e implementar los métodos respondsToSelector: y forwardInvocation: en el proxy para que pasen solo los permitidos. Puede ser una mejor opción si se usa mucho esta técnica. Para un caso puntual como este, la sobrecarga de respondsToSelector: es mucho más simple.