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.

lunes 12 de diciembre de 2011

Serie numérica

Hace un tiempo en un pizarrón en la oficina apareció la siguiente serie:

1
11
21
1211
111221
312211
13112221

1113213211

Algunas preguntas:
  1. ¿Cuál es el próximo elemento de la serie y como se construye?
  2. ¿Cuál es el dígito más grande que puede aparecer? Tip: es menor que 9 :)
  3. Tengo una conjetura: el largo de un elemento de la serie no puede ser menor que el largo del elemento anterior. ¿Alguien se anima a demostrarlo?

sábado 10 de diciembre de 2011

Sobre la comparación entre lenguaje de programación

El otro día en un almuerzo salió el tema de si tiene sentido comparar distintos lenguajes de programación.

Una de las posturas era que en realidad, con cualquier lenguaje de programación moderno (léase Java, C#, Objective-C, Ruby, etc.) puedo solucionar cualquier problema que pueda solucionar con otro de estos lenguajes.

De todas formas yo creo que sí se pueden comparar, y que si bien con cualquiera se pueden resolver los problemas, no se obtiene la misma productividad.

Voy a poner un ejemplo, usando una muy pequeña característica de los lenguajes de programación: los loops de tipo "for".

El caso más simple, es querer recorrer una colección partiendo del primer item hasta el último, avanzando de a uno. Para eso me alcanza una construcción del tipo:

for index = 0 to count-1 {
  // hacer algo con index
}


¿Qué pasa ahora si quiero contar de dos en dos? ¿Me sirve la construcción anterior? Bueno, sí... puedo escribir lo siguiente:

for fakeIndex = 0 to (count-1)/2 {
  index = fakeIndex * 2
  // hacer algo con index
}


No es complicado, pero no parece muy práctico. Un lenguaje que le pueda decir el "step" al comando "for" no es más potente, pero sí más práctico. La forma de escribirlo quedaría entonces:

for index = 0 to count-1 step 2 {
  // hacer algo con index
}


El problema que tiene esto, es que si quiero que el step sea variable, por ejemplo quiero iterar sobre las potencias de 2, con el step solo no me alcanza. Lo puedo resolver, pero queda más complicado. Una solución es usar la sintaxis que usa C, donde se le indica el valor inicial, la condición de parada, y el incremento. Así:

for (int index = 1; index <= someValue; index *= 2) {
  // hacer algo con index
}


Esta iteración sí la puedo construir con el primer tipo de "for", pero es más difícil: hay que pensar como mapear el índice de la iteración con el índice que realmente quiero manejar.

De todas formas, cuando uno tiene este "for" como es el caso de C y todos sus lenguajes derivados, lo más común es usarlo para acceder a los elementos de una colección:

for (int index = 0; index < array.count; index++) {
  value = array[index];
  // hacer algo con value
}


En general, el índice se termina usando solo para acceder al elemento correspondiente de la colección. El valor inicial, el final y el incremento son una construcción artificial. Lo que realmente quiero es poder iterar sobre los elementos de mi colección.

Supongamos ahora que tengo una construcción que me permite hacer esto:

for (value in array) {
  // hacer algo con value
}


¿Es esta construcción más potente que la anterior? No, las dos permiten expresar lo mismo. Pero sin duda es mucho más conveniente, aumenta la productividad y minimiza el riesgo de cometer errores (por ejemplo si uso "index <= array.count" en vez de "index < array.count", error bastante común).

¿Se puede hacer mejor? Sí, se puede, y lo curioso del caso es que para la forma de mejorarlo es eliminar la construcción "for", que es lo que se tiene en Ruby (donde sí existe el "for" pero no se usa...).

Si no tengo el comando "for", entonces tengo que buscar otra forma de iterar sobre una colección o conjunto de valores. Esto en Ruby se resuelve con el método "each" (o "each_index") de la siguiente forma:

array.each do |value|
  # hacer algo con value
end


o

(0..5).each do |index|
  # hacer algo con index
end


¿Por qué esto es mejor? Porque si el "each" es un método y no una construcción del lenguaje, quiere decir que el lenguaje me permite iterar de la forma que quiera. Ahora puedo separar el uso del "for" según para que lo quiera.

Las cosas más comunes para las que recorro una colección son para transformar los elementos en otra colección, filtrar los valores según alguna condición, o acumular en una variable. Entonces puedo tener métodos que hagan estas cosas, y en mi programa no voy a necesitar la iteración, porque en realidad lo que quería no era iterar sino hacer algo de más alto nivel a la colección.

En conclusión, es verdad que no hay nada que no pueda resolver con las construcciones más simples, y por lo tanto no hay un lenguaje más potente que otro, lo que sí hay son lenguajes más productivos.


Actualización, 12/12/11 8:56: Willy me encontró un bug, así que lo corregí... Había un loop que empezaba en cero e incrementaba multiplicando por dos, por lo que nunca iba a salir...

lunes 14 de noviembre de 2011

RubyConf Uruguay 2011

Los días 11 y 12 de noviembre se llevó a cabo el evento RubyConf Uruguay 2011. Este año (es la segunda vez que se hace) tuve la suerte de poder ir.

El evento fue en el Auditorio de Antel que está muy bueno, nunca había ido.

El formato del evento fue de charlas de 30 minutos, de a una por vez (había una sola sala).

Oradores

Una cosa que me sorprendió fue el buen nivel de los oradores. Trajeron gente de varias partes del mundo (Argentina, Brasil, Chile, Estados Unidos, Alemania, Australia, Japón y seguramente alguno más...). Pero no solo eso, trajeron algunas personalidades importantes del mundo de Ruby, como por ejemplo Scott Chacon, uno de los creadores de Git, o Blake Mizerany, creador de Sinatra.

Una curiosidad con respecto a los oradores internacionales: varios de habla inglesa dieron su charla en español, pero todos los brasileros que vi (3 por lo menos) la dieron en inglés...

Audiencia

Siendo un evento técnico, para desarrolladores, la audiencia éramos todos (o la gran mayoría) desarrolladores.

Otra característica es que la inmensa mayoría eran hombres. Diría que un 95%, pero tengo miedo de quedarme corto...

Con respecto a la vestimenta de la gente, era sumamente informal. Y lo digo yo que durante años fui vestido de forma informal (pero no tanto) a los encuentros GeneXus... Había por ejemplo gente de la organización de short y chinelas, o expositores que dieron la charla de bermudas.

Otra cosa que me llamó la atención es que un buen número de las personas que asistieron estaban con su notebook, y lo tenían abierto y prendido durante las charlas. Los notebooks, habían muchas Macs, y las que no eran Macs por lo que se veía tenían Linux. Creo que no vi a nadie usar un Windows.

Organización

La organización estuvo bastante bien:
  • el wi-fi funcionó muy bien todo el tiempo,
  • las charlas empezaron y terminaron con una puntualidad bastante aceptable (claro, uno se mal acostumbra y lo compara con los encuentros GeneXus...),
  • el lugar en sí está bueno para un evento de este tamaño.

Charlas

Con respecto a las charlas, hubo momentos donde me perdí un poco, porque al no estar metido de lleno en la tecnología hay cosas que me resultaron demasiado específicas.

De todas formas, hubo varias que me gustaron. Creo que se pueden ver acá (aunque no probé...)
  • A Tale of Three Trees, por Scott Chacon sobre git, y más específicamente sobre el comando git-reset y como maneja internamente la última versión confirmada, los cambios para confirmar y el directorio de trabajo.
  • JRuby: Introduciendo Ruby en el mundo enterprise, por Jano Gonzalez.
  • Lean Startups for the Ruby Hacker, por Evan Henshaw-Plath.
  • Winning strategies in the battle against code smells, por Nigel Fernandes.
Hubo varios temas recurrentes, como metodologías ágiles de desarrollo, TDD o pair programming. De este último me llama la atención, porque hay varios que dijeron que usan pair programming todo el tiempo. Yo le veo la utilidad, pero más para cosas específicas. No se si me convencen de hacer pair programming todo el tiempo...

Conclusión

Me gustó el evento, creo que valió la pena ir. Sirve para salir un poco de lo que uno está acostumbrado y ver en que anda la gente de otras comunidades.

miércoles 9 de noviembre de 2011

Por qué creo que Objective-C es mejor que Java

Hace unos días @GMilano hacía una comparación en Twitter donde decía que
Android vs iOS
IDEs:Eclipse > XCode
Language: Java > Objective-C
Framework: Cocoa > Android
Emulator: iOS > Android
Analysis: iOS > Android
y mi respuesta fue que
Casi de acuerdo, solo que Objective-C > Java... Pero, no es algo evidente al principio.
Esa respuesta tiene tiene una justificación, pero como verán lleva un poco más de 140 caracteres.

Aclaraciones:
  1. Trabajo con Objective-C desde hace unos dos años, pero hace mucho que no programo en Java en serio. Si cometo algún error en la comparación agradezco que me lo hagan notar.
  2. Hasta hace no mucho, tenía la idea de que todos los lenguajes de programación "modernos" eran más o menos equivalentes. Si tienen esa misma idea, primero lean este artículo de Joel Spolsky: Can your programming language do this?
  3. Muchas veeces uno no puede elegir (iOS = Objective-C, Android = Java), pero no por eso la comparación deja de ser válida :)
Extensibilidad de clases estándar

En Objective-C, uno puede agregar métodos a las clases estándar. Digamos por ejemplo que uno quiere agregar el método year a la clase NSDate para invocarlo con
int year = [myDate year];
en vez de hacer
NSDateComponents *comp = [[NSCalendar currentCalendar] components:NSYearCalendarUnit fromDate:myDate];
int year = [comp year];
Eso se puede hacer de forma muy simple, creando una "category" con la implementación
@interface NSDate (Helpers)
- (int)year;
@end
@implementation NSDate
- (int)year {
    NSDateComponents *comp = [[NSCalendar currentCalendar] components:NSYearCalendarUnit fromDate:self];
    return [comp year];
}
@end
Esto en Java no es posible. Se puede crear una subclase, o hacer un método estático en alguna otra clase, pero no modificar las clases ya existentes.

Blocks y selectors

En Objective-C, se pueden definir métodos que reciban un bloque de código o un "selector" (básicamente es el nombre de un método). En Java el pasaje de funciones por parámetro es bastante más complicado e involucra crear una clase específica que contenga el método.

Pasar funciones o bloques de código por parámetro permite por ejemplo implementar el método map: en la clase NSArray (combinando con lo que decía más arriba de las categories) de esta forma
- (NSArray *)map:(id (^)(id element))block {
  if (!self) {
    return nil;
  }
  NSMutableArray *tempArray = [[NSMutableArray alloc] initWithCapacity:[self count]];
  for (id element in self) {
    [tempArray addObject:block(element)];
  }
  return [tempArray autorelease];
}
¿Cuál es la gracia? Que si quiero construir un array a partir de otro, solo tengo que pasarle al método map: el bloque de código de la transformación, sin tener que escribir todo el código accesorio: crear el nuevo array, iterar, manejo de memoria, casos de borde (if !self), etc.

Además si en algún momento se hace alguna optimización o algún arreglo, queda para todos los que lo usan.

Manejo de memoria

En lo que tiene que ver con el manejo de memoria, tanto Objective-C como Java tienen Garbage Collector.

Claro, eso no es cierto para el caso de iOS... donde Objective-C usa una técnica llamada "reference counting". Si bien esto es más complejo para el programador, es mejor en tiempo de ejecución (si se hace bien) porque la memoria se libera en el momento que se deja de usar, no hay que esperar por un proceso que lo haga (GC)

Antes de iOS 5, era el programador que tenía que encargarse de retener/liberar los objetos, cosa que es bastante tediosa al principio y genera buena parte de los errores que cometen los novatos.

Con la liberación de iOS 5 y las herramientas asociadas, se incluye una nueva opción: dejar que el compilador se encargue. Esto se llama Automatic Reference Counting (ARC) y tiene lo mejor de los dos mundos: es mejor en tiempo de ejecución y no es una carga para el programador.

Sintaxis

Con respecto a la sintaxis, tengo que reconocer que ahí sí, Java le gana  a Objective-C. Tener que acceder a un elemento de un array con
[myArray objectAtIndex:0];
es bastante tedioso...

Por eso decía en Twitter que no es evidente que Objective-C es mejor que Java, porque la primer impresión que uno se lleva con el lenguaje es que es bastante complicado, sintácticamente hablando.

Conclusión

Estas son algunas de las diferencias que tienen ambos lenguajes. Seguramente hay muchas más, y alguno tendrá votos a favor para cualquiera de los dos lenguajes. Lo que quería con este post era justificar mi respuesta en Twitter, dando mi punto de vista al respecto.

En una próxima entrega agregaré también la comparación con C# y Ruby...