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.

No hay comentarios.:

Publicar un comentario