Ulzurrun de Asanza i Sàez

Month: September 2011

Actualización 2.0.1 de Music Maniac

Icono de Music Maniac Lite 2.0En cuestión de horas estará disponible la versión 2.0.1 de Music Maniac en el AppStore. Esta actualización corrige algunos errores de la versión 2.0, entre los que se encuentran:

También incluye otros cambios menores, como que al jugar Offline y salir de un concurso, en lugar de perder los avances, se carga la vista de estadísticas y se guardan los puntos obtenidos durante el tiempo que se jugó.

Además esta actualización también afecta a Music Maniac Lite, pudiendo disfrutar de las novedades de la versión 2.0 en la versión reducida de Music Maniac.


Guardar objetos en archivos con Objective-C

Ayer Carmen Parra preguntaba en un comentario cómo se pueden guardar los datos introducidos por el usuario en un archivo. Como el proceso es demasiado largo como para publicarlo en un comentario, he decidido escribir este pequeño tutorial.

NSArray, NSDictionary, NSString, NSNumber, NSData y otros

Resultado del código de ejemploHay dos métodos sencillos para almacenar un objeto en un archivo y poder recuperarlo cuando nos convenga. El primero no se puede aplicar a cualquier objeto, sino a objetos de algunas clases de Objective-C que responden a dos métodos llamados writeToFile:atomically: y writeToURL:atomically:. El uso de estos métodos es muy sencillo, basta con llamarlos en el momento en el que queramos que el objeto se guarde en un archivo.

A continuación teneís un ejemplo comentado. Básicamente creamos una matriz, le añadimos algo de contenido, la imprimimos, la guardamos en un archivo y nos deshacemos de ella. A continuación creamos otra matriz que lea el contenido del archivo y comprobamos que tiene el mismo contenido que la anterior. El resultado que obtendréis será similar al de la imagen de la derecha.

[obj-c]
NSMutableArray *array = [[NSMutableArray alloc] init]; // Creamos una matriz
[array addObject:@"¡Hola"]; // Añadimos un objeto
[array addObject:@"mundo!"]; // Añadimos otro objeto
NSLog(@"Matriz original: %@", array); // Imprimimos la matriz original
[array writeToFile:@"filename" atomically:YES]; // Escribimos en el archivo filename el contenido de la matriz
[array release]; // Liberamos memoria

NSMutableArray *arrayFromFile = [[NSMutableArray alloc] initWithContentsOfFile:@"filename"]; // Esta matriz tiene el mismo contenido que tenía la matriz anterior
NSLog(@"%@", arrayFromFile); // Imprimimos la matriz recuperada del archivo
[arrayFromFile release]; // Liberamos memoria
[/obj-c]

Del mismo modo que se puede guardar en un archivo un objeto de la clase NSArray, también se puede usar con objetos NSDictionary, NSString, NSData y NSNumber, entre otros. Sin embargo no se puede usar con objetos de una clase creada por nosotros (en la mayoría de los casos).

Clases creadas por nosotros

Para guardar en un archivo un objeto de una clase creada por nosotros, tendremos que hacer que el objeto sigua el protocolo NSCoding. Tras esto, mediante las clases NSKeyedArchiver y NSKeyedUnarchiver podremos guardar en un archivo y leer de un archivo nuestro objeto. Este protocolo tiene dos métodos necesarios: initWithCoder: y encodeWithCoder:.

Vamos a suponer que hemos creado la clase Empleado, que tiene la siguiente interfaz:

[obj-c]
@interface Empleado : NSObject

@property (nonatomic, retain) NSString *nombre, *apellidos;
@property NSUInteger edad;
@property BOOL esEmpleadoDelMes;

@end
[/obj-c]

Para poder almacenar este objeto en un archivo tendremos primero que hacer que siga el protocolo NSCoding. Modficaremos la interfaz y la dejaremos así:

[obj-c]
#define kNombre @"Nombre"
#define kApellidos @"Apellidos"
#define kEdad @"Edad"
#define kEsEmpleadoDelMes @"esEmpleadoDelMes"

@interface Empleado : NSObject

@property (nonatomic, retain) NSString *nombre, *apellidos;
@property NSUInteger edad;
@property BOOL esEmpleadoDelMes;

@end

[/obj-c]

Básicamente hay dos cambios: hemos indicado que la clase sigue el protocolo NSCoding y hemos definido una serie de constantes que usaremos más adelante y que entenderéis en seguida su utilidad.

Ahora pasamos a la implementación. Esta es la implementación original de la clase Empleado, antes de seguir el protocolo NSCoding:

[obj-c]
@implementation Empleado

@synthesize nombre, apellidos;
@synthesize edad;
@synthesize esEmpleadoDelMes;

– (void)dealloc
{
[nombre release];
[apellidos release];

[super dealloc];
}

@end
[/obj-c]

Ahora vamos a añadir los dos métodos que necesitamos para seguir el protocolo NSCoding. Estos métodos nos permiten archivar nuestro objeto. El método encodeWithCoder: tiene por parámetro un objeto de la clase NSCoder. En este objeto archivaremos las propiedades del nuestro, asignándole a una serie de claves las propiedades del objeto.

Por ejemplo, si queremos hacer que la propiedad nombre se archive en la clave clave, el método tendría el siguiente aspecto:

[obj-c]
– (void)encodeWithCoder:(NSCoder *)coder {

[coder encodeObject:self.nombre forKey:@"clave"];

}
[/obj-c]

No todas las propiedades se archivan de la misma forma. Los objeto se archivan a través del método encodeObject:forKey:, los números enteros se archivan con encodeInteger:forKey: y los valores booleanos con encodeBool:forKey:. Las claves son muy importantes, ya que el método initWithCoder: va a desarchivar el objeto usando las mismas claves para acceder a las propiedades, por eso hemos definido una serie de constantes que serán las claves que usaremos para archivar y desarchivar las propiedades.

Nota: Tenéis una lista de todos los métodos para archivar objetos, así como una explicación del proceso de archivar y desarchivar objetos en la documentación de Apple (en inglés).

Nuestro método encodeWithCoder: quedará así:

[obj-c]
– (void)encodeWithCoder:(NSCoder *)coder {

[coder encodeObject:self.nombre forKey:kNombre];
[coder encodeObject:self.apellidos forKey:kApellidos];
[coder encodeInteger:self.edad forKey:kEdad];
[coder encodeBool:self.esEmpleadoDelMes forKey:kEsEmpleadoDelMes];

}
[/obj-c]

El método initWithCoder: es muy similar al anterior, sólo que su funcionamiento es el contrario: a partir de un objeto archivado se obtienen las propiedades y se crea un objeto de la clase Empleado (en este caso) con las propiedades que tenía el objeto al archivarse. En este caso enviaremos el mensaje decodeObjectForKey:@”clave” a coder y asignaremos el valor devuelto a la propiedad de nuestro objeto. De nuevo, se usan métodos diferentes para decodificar número enteros, valores booleanos y otros tipos de variables. Nuestro método initWithCoder: quedará así:

[obj-c]
– (id)initWithCoder:(NSCoder *)coder {

if ((self = [super init])) {

self.nombre = [coder decodeObjectForKey:kNombre];
self.apellidos = [coder decodeObjectForKey:kApellidos];
self.edad = [coder decodeIntForKey:kEdad];
self.esEmpleadoDelMes = [coder decodeBoolForKey:kEsEmpleadoDelMes];

}

return self;
}
[/obj-c]

Resultado del ejemploAhora pasemos a crear un objeto Empleado, a guardarlo en un archivo y a comprobar que funciona correctamente. Primero incluímos la interfaz de la clase Empleado en el archivo en el que vamos a crear el objeto, en mi caso, y al tratarse de una “Command Line Tool“, lo incluiré en el archivo main.m. El siguiente código comentado crea un objeto de la clase Empleado, le asigna algunos valores sus propiedades, lo archiva y después crea un nuevo objeto con el contenido del archivo en el que se ha guardado antes el objeto:

[obj-c]
Empleado *trabajador = [[Empleado alloc] init]; // Creamos el objeto
trabajador.nombre = @"Juan"; // Le damos un nombre
trabajador.apellidos = @"García López"; // Unos apellidos
trabajador.edad = 26; // Una edad
trabajador.esEmpleadoDelMes = YES; // Y lo hacemos empleado del mes

NSLog(@"%@ %@ tiene %lu años", trabajador.nombre, trabajador.apellidos, trabajador.edad); // Imprimimos sus datos

[NSKeyedArchiver archiveRootObject:trabajador toFile:@"empleado"]; // Lo guardamos en el archivo empleado

[trabajador release]; // Liberamos memoria

Empleado *trabajadorRecuperado = [NSKeyedUnarchiver unarchiveObjectWithFile:@"empleado"]; // Creamos un objeto a partir del archivo empleado

NSLog(@"%@ %@ tiene %lu años", trabajadorRecuperado.nombre, trabajadorRecuperado.apellidos, trabajadorRecuperado.edad); // Imprimimos sus datos
[/obj-c]

Así se puede guardar un objeto de cualquier clase en un archivo, de forma que puede ser recuperado en cualquier momento. Los objetos más complejos pueden necesitar métodos más complejos a la hora de archivarlos. Del mismo modo no se tienen por qué archivar todas las propiedades si no se quiere.

Por último dejo a vuestra disposición los archivos del proyecto que he creado mientras escribía el tutorial, por si os atascáis y necesitáis un empujoncito.