Almacenar objetos personalizados en NSMutableArray en NSUserDefaults

101

Recientemente he intentado almacenar los resultados de búsqueda de mi aplicación de iPhone en la colección NSUserDefaults. También utilizo esto para guardar la información de registro del usuario con éxito, pero por alguna razón, intentar almacenar mi NSMutableArray de clases de ubicación personalizadas siempre vuelve vacío.

Intenté convertir NSMutableArray en un elemento NSData a partir de esta publicación, pero obtengo el mismo resultado (¿Es posible guardar una matriz de enteros usando NSUserDefaults en iPhone? )

Los ejemplos de código que he probado son:

Salvar:

[prefs setObject:results forKey:@"lastResults"];
[prefs synchronize];

o

NSData *data = [NSData dataWithBytes:&results length:sizeof(results)];
[prefs setObject:data forKey:@"lastResults"];

o

NSData *data = [NSKeyedArchiver archivedDataWithRootObject:results];
[prefs setObject:data forKey:@"lastResults"];

Carga:

lastResults = (NSMutableArray *)[prefs objectForKey:@"lastResults"];

o

NSData *data = [prefs objectForKey:@"lastResults"];
memcpy(&lastResults, data.bytes, data.length);  

o

NSData *data = [prefs objectForKey:@"lastResults"];
lastResults = [NSKeyedUnarchiver unarchiveObjectWithData:data];

Después de seguir los consejos a continuación, también he implementado NSCoder en mi objeto (ignore el uso excesivo de NSString es temporal):

#import "Location.h"


@implementation Location

@synthesize locationId;
@synthesize companyName;
@synthesize addressLine1;
@synthesize addressLine2;
@synthesize city;
@synthesize postcode;
@synthesize telephoneNumber;
@synthesize description;
@synthesize rating;
@synthesize priceGuide;
@synthesize latitude;
@synthesize longitude;
@synthesize userLatitude;
@synthesize userLongitude;
@synthesize searchType;
@synthesize searchId;
@synthesize distance;
@synthesize applicationProviderId;
@synthesize contentProviderId;

- (id) initWithCoder: (NSCoder *)coder
{
    if (self = [super init])
    {
        self.locationId = [coder decodeObjectForKey:@"locationId"];
        self.companyName = [coder decodeObjectForKey:@"companyName"];
        self.addressLine1 = [coder decodeObjectForKey:@"addressLine1"];
        self.addressLine2 = [coder decodeObjectForKey:@"addressLine2"];
        self.city = [coder decodeObjectForKey:@"city"];
        self.postcode = [coder decodeObjectForKey:@"postcode"];
        self.telephoneNumber = [coder decodeObjectForKey:@"telephoneNumber"];
        self.description = [coder decodeObjectForKey:@"description"];
        self.rating = [coder decodeObjectForKey:@"rating"];
        self.priceGuide = [coder decodeObjectForKey:@"priceGuide"];
        self.latitude = [coder decodeObjectForKey:@"latitude"];
        self.longitude = [coder decodeObjectForKey:@"longitude"];
        self.userLatitude = [coder decodeObjectForKey:@"userLatitude"];
        self.userLongitude = [coder decodeObjectForKey:@"userLongitude"];
        self.searchType = [coder decodeObjectForKey:@"searchType"];
        self.searchId = [coder decodeObjectForKey:@"searchId"];
        self.distance = [coder decodeObjectForKey:@"distance"];
        self.applicationProviderId = [coder decodeObjectForKey:@"applicationProviderId"];
        self.contentProviderId = [coder decodeObjectForKey:@"contentProviderId"];
    }
}

- (void) encodeWithCoder: (NSCoder *)coder
{
    [coder encodeObject:locationId forKey:@"locationId"];
    [coder encodeObject:companyName forKey:@"companyName"];
    [coder encodeObject:addressLine1 forKey:@"addressLine1"];
    [coder encodeObject:addressLine2 forKey:@"addressLine2"];
    [coder encodeObject:city forKey:@"city"];
    [coder encodeObject:postcode forKey:@"postcode"];
    [coder encodeObject:telephoneNumber forKey:@"telephoneNumber"];
    [coder encodeObject:description forKey:@"description"];
    [coder encodeObject:rating forKey:@"rating"];
    [coder encodeObject:priceGuide forKey:@"priceGuide"];
    [coder encodeObject:latitude forKey:@"latitude"];
    [coder encodeObject:longitude forKey:@"longitude"];
    [coder encodeObject:userLatitude forKey:@"userLatitude"];
    [coder encodeObject:userLongitude forKey:@"userLongitude"];
    [coder encodeObject:searchType forKey:@"searchType"];
    [coder encodeObject:searchId forKey:@"searchId"];
    [coder encodeObject:distance forKey:@"distance"];
    [coder encodeObject:applicationProviderId forKey:@"applicationProviderId"];
    [coder encodeObject:contentProviderId forKey:@"contentProviderId"];

}
Anthony Main
fuente

Respuestas:

177

Para cargar objetos personalizados en una matriz, esto es lo que he usado para tomar la matriz:

NSUserDefaults *currentDefaults = [NSUserDefaults standardUserDefaults];
NSData *dataRepresentingSavedArray = [currentDefaults objectForKey:@"savedArray"];
if (dataRepresentingSavedArray != nil)
{
    NSArray *oldSavedArray = [NSKeyedUnarchiver unarchiveObjectWithData:dataRepresentingSavedArray];
    if (oldSavedArray != nil)
        objectArray = [[NSMutableArray alloc] initWithArray:oldSavedArray];
    else
        objectArray = [[NSMutableArray alloc] init];
}

Debe verificar que los datos devueltos por los valores predeterminados del usuario no sean nulos, porque creo que desarchivar desde nil causa un bloqueo.

Archivar es simple, usando el siguiente código:

[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:objectArray] forKey:@"savedArray"];

Como señaló f3lix, debe hacer que su objeto personalizado cumpla con el protocolo NSCoding. Agregar métodos como el siguiente debería funcionar:

- (void)encodeWithCoder:(NSCoder *)coder;
{
    [coder encodeObject:label forKey:@"label"];
    [coder encodeInteger:numberID forKey:@"numberID"];
}

- (id)initWithCoder:(NSCoder *)coder;
{
    self = [super init];
    if (self != nil)
    {
        label = [[coder decodeObjectForKey:@"label"] retain];
        numberID = [[coder decodeIntegerForKey:@"numberID"] retain];
    }   
    return self;
}
Brad Larson
fuente
3
Faltaba un "return self" al final de su método initWithCoder:. Agregar eso parece permitir que se lleve a cabo el desarchivado.
Brad Larson
2
No debe almacenar matrices en nsuserdefaults ya que las matrices podrían crecer demasiado para nsuserdefaults. En su lugar, debe almacenarse en un archivo usando + (BOOL) archiveRootObject: (id) rootObject toFile: (NSString *) ruta. ¿Por qué se votó tanto esta respuesta?
mskw
1
@mskw: tiene razón en que NSUserDefaults no debe usarse para el almacenamiento de matrices grandes (se deben usar Core Data o SQLite sin procesar para eso), pero está perfectamente bien almacenar pequeñas matrices de objetos codificados allí. En cualquier caso, la pregunta era cómo hacer esto, que es lo que proporciona el código anterior. En cuanto a los votos, tu conjetura es tan buena como la mía. Mucha gente debe haber querido hacer esto durante los últimos cuatro años.
Brad Larson
2
@Goodsum - No, eso funciona bien. Pruébelo, devolverá un NSMutableArray que de hecho es mutable. Simplemente inicia la nueva matriz agregando esa matriz de elementos.
Brad Larson
1
@AlbertRenshaw - ¿De dónde provienen los andSyncque agregaste al código anterior? Esa no es una firma de método real para NSUserDefaults. Además, synchronizeno es tan útil como antes: twitter.com/Catfish_Man/status/647274106056904704
Brad Larson
13

Creo que ha recibido un error en su método initWithCoder, al menos en el código proporcionado no devuelve el objeto 'self'.

- (id) initWithCoder: (NSCoder *)coder
{
    if (self = [super init])
    {
        self.locationId = [coder decodeObjectForKey:@"locationId"];
        self.companyName = [coder decodeObjectForKey:@"companyName"];
        self.addressLine1 = [coder decodeObjectForKey:@"addressLine1"];
        self.addressLine2 = [coder decodeObjectForKey:@"addressLine2"];
        self.city = [coder decodeObjectForKey:@"city"];
        self.postcode = [coder decodeObjectForKey:@"postcode"];
        self.telephoneNumber = [coder decodeObjectForKey:@"telephoneNumber"];
        self.description = [coder decodeObjectForKey:@"description"];
        self.rating = [coder decodeObjectForKey:@"rating"];
        self.priceGuide = [coder decodeObjectForKey:@"priceGuide"];
        self.latitude = [coder decodeObjectForKey:@"latitude"];
        self.longitude = [coder decodeObjectForKey:@"longitude"];
        self.userLatitude = [coder decodeObjectForKey:@"userLatitude"];
        self.userLongitude = [coder decodeObjectForKey:@"userLongitude"];
        self.searchType = [coder decodeObjectForKey:@"searchType"];
        self.searchId = [coder decodeObjectForKey:@"searchId"];
        self.distance = [coder decodeObjectForKey:@"distance"];
        self.applicationProviderId = [coder decodeObjectForKey:@"applicationProviderId"];
        self.contentProviderId = [coder decodeObjectForKey:@"contentProviderId"];
    }

    return self; // this is missing in the example above


}

yo suelo

NSData *data = [NSKeyedArchiver archivedDataWithRootObject:results];
[prefs setObject:data forKey:@"lastResults"];

y

NSUserDefaults *currentDefaults = [NSUserDefaults standardUserDefaults];
NSData *dataRepresentingSavedArray = [currentDefaults objectForKey:@"lastResults"];
if (dataRepresentingSavedArray != nil)
{
        NSArray *oldSavedArray = [NSKeyedUnarchiver unarchiveObjectWithData:dataRepresentingSavedArray];
        if (oldSavedArray != nil)
                objectArray = [[NSMutableArray alloc] initWithArray:oldSavedArray];
        else
                objectArray = [[NSMutableArray alloc] init];
}

y funciona perfecto para mí.

Con saludos,

Stefan


fuente
3
Stephan eres el salvavidas, mira aquí; Esa línea me salvó la mente "if (self = [super init])" stackoverflow.com/questions/1933285/…
fyasar
Obtuve ERROR_BAD_ACCESS pero me salvaste. Estaba usando la propiedad con retener solo y agregando la variable self.Lo resolvió.
David
0

Creo que parece que el problema con su código no es guardar la matriz de resultados. Está cargando los datos, intente usar

lastResults = [prefs arrayForKey:@"lastResults"];

Esto devolverá la matriz especificada por la clave.

Gcoop
fuente
0

Consulte "¿Por qué NSUserDefaults no pudo guardar NSMutableDictionary en iPhone SDK?" ( ¿Por qué NSUserDefaults no pudo guardar NSMutableDictionary en iPhone SDK? )


Si desea (des) serializar objetos personalizados, debe proporcionar las funciones para (des) serializar los datos (protocolo NSCoding). La solución a la que hace referencia funciona con la matriz int porque la matriz no es un objeto, sino una porción contigua de memoria.

f3lix
fuente
Creo que tienes razón, echaré un vistazo a NSCoding (a menos que tengas un enlace recomendado), los objetos en sí están hechos en NSStrings, eso es todo, así que supongo que podría escribir mi propio serializador y ponerlos todos en una matriz de cadenas
Anthony Main
Ok, he implementado NSCode ver arriba, pero todavía no me gusta usar NSArchiver
Anthony Main
0

Recomendaría no intentar almacenar cosas como esta en la base de datos predeterminada.

SQLite es bastante fácil de aprender y usar. Tengo un episodio en uno de mis screencasts ( http://pragprog.com/screencasts/v-bdiphone ) sobre una envoltura simple que escribí (puede obtener el código sin comprar el SC).

Es mucho más limpio almacenar datos de aplicaciones en el espacio de aplicaciones.

Dicho todo esto, si todavía tiene sentido poner estos datos en la base de datos predeterminada, vea la publicación f3lix publicada.

Bill Dudney
fuente
1
Gracias por la sugerencia, pero realmente no quiero tener que comenzar a escribir consultas SQL (como veo en su ejemplo) solo para un objeto con algunas cadenas
Anthony Main