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.

2 comentarios:

  1. Hola,

    No me queda claro si esta jugando con el Meta Modelo de Objective-C o es un cambio de verdad ?

    Si estas jugando con el meta modelo me parece bárbaro pero no haría ese cambio en un sistema.

    Si estabas jugando obviar lo siguiente:

    El #respondsToSelector: se debe llamar de muchos lados y ensuciar el método con algo de Views no me parece correcto.
    Fíjate los senders de (quien llama a) #respondsToSelector:.

    Por otro lado me parece que lo que etas haciendo es "engañar" a Objective-C para cambiar el camino de ejecución, pero el método en si mismo existe en la clase. Es decir, no lo sacas y pones del sistema en runtime, sino que bajo determinadas condiciones el método es "invisible" para quien usa ("tiene contrato con") #respondsToSelector:.

    Si hago algo así el método existe:
    (ojo esta en Smalltalk porque desconozco Objective-C)

    MyClass methodDictionary at: (#tableView:viewForHeaderInSection:)

    Y el método en la clase esta, o me algo se me escapa ?

    Saludos,
    Bruno

    ResponderEliminar
  2. Bruno:

    El respondsToSelector: se llama efectivamente de muchos lados, y no es "ensuciarlo con algo de views", porque la clase en la que hice override del método es una view...

    Es correcto lo que decís que el método sigue existiendo, y que si lo llamo directamente va a funcionar. Lo que pasa es que en este caso está definido como opcional en un protocolo, por lo tanto siempre me van a preguntar si mi clase responde a ese selector, antes de invocarlo.

    ResponderEliminar