Armazenamento de objetos personalizados em um NSMutableArray em NSUserDefaults

101

Recentemente, tenho tentado armazenar os resultados da pesquisa do meu aplicativo para iPhone na coleção NSUserDefaults. Eu também uso isso para salvar as informações de registro do usuário com êxito, mas, por algum motivo, tentar armazenar meu NSMutableArray de classes de localização personalizadas sempre volta vazio.

Tentei converter o NSMutableArray em um elemento NSData a partir desta postagem, mas obtenho o mesmo resultado (é possível salvar uma matriz inteira usando NSUserDefaults no iPhone? )

Os exemplos de código que experimentei são:

Salve :

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

ou

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

ou

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

Carga:

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

ou

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

ou

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

Depois de seguir os conselhos abaixo, também implementei o NSCoder em meu objeto (ignore o uso excessivo de NSString, seu temporário):

#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
fonte

Respostas:

177

Para carregar objetos personalizados em um array, isto é o que eu usei para pegar o array:

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];
}

Você deve verificar se os dados retornados dos padrões do usuário não são nulos, porque acredito que desarquivar de nulos causa um travamento.

O arquivamento é simples, usando o seguinte código:

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

Como f3lix apontou, você precisa fazer com que seu objeto personalizado esteja em conformidade com o protocolo NSCoding. Adicionar métodos como o seguinte deve resolver o problema:

- (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
fonte
3
Estava faltando um "retorno próprio" no final do seu método initWithCoder :. Adicionar isso parece permitir que o desarquivamento ocorra.
Brad Larson
2
Você não deve armazenar arrays em nsuserdefaults, pois os arrays podem ficar muito grandes para nsuserdefaults. Ele deve ser armazenado em um arquivo usando + (BOOL) archiveRootObject: (id) rootObject toFile: (NSString *) path. Por que essa resposta foi tão votada?
mskw
1
@mskw - Você está certo em que NSUserDefaults não deve ser usado para armazenamento de grandes arrays (Core Data ou SQLite bruto deve ser usado para isso), mas é perfeitamente normal esconder pequenos arrays de objetos codificados lá. Em qualquer caso, a questão era como fazer isso, que é o que o código acima fornece. Quanto aos votos, seu palpite é tão bom quanto o meu. Muitas pessoas devem ter desejado fazer isso nos últimos quatro anos.
Brad Larson
2
@Goodsum - Não, isso funciona muito bem. Experimente, ele retornará um NSMutableArray que é realmente mutável. Ele simplesmente inicia o novo array adicionando aquele array de itens a ele.
Brad Larson
1
@AlbertRenshaw - De onde veio o que andSyncvocê adicionou ao código acima? Essa não é uma assinatura de método real para NSUserDefaults. Além disso, synchronizenão é tão útil quanto antes: twitter.com/Catfish_Man/status/647274106056904704
Brad Larson
13

Acho que você obteve um erro no seu método initWithCoder, pelo menos no código fornecido você não retorna o 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


}

eu uso

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

e

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];
}

e funciona perfeitamente para mim.

Atenciosamente,

Stefan


fonte
3
Stephan você é o salva-vidas, olhe aqui; Essa linha salvou minha mente "if (self = [super init])" stackoverflow.com/questions/1933285/…
fyasar
Eu tenho ERROR_BAD_ACCESS, mas você me salvou. Estava usando a propriedade apenas com reter e adicionando a variável self.
David,
0

Acho que parece que o problema com o seu código não é salvar a matriz de resultados. Está carregando os dados, tente usar

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

Isso retornará a matriz especificada pela chave.

Gcoop
fonte
0

Consulte "Por que NSUserDefaults falhou ao salvar NSMutableDictionary no iPhone SDK?" ( Por que NSUserDefaults falhou ao salvar NSMutableDictionary no iPhone SDK? )


Se você deseja (des) serializar objetos personalizados, é necessário fornecer as funções para (des) serializar os dados (protocolo NSCoding). A solução à qual você se refere funciona com o array int porque o array não é um objeto, mas um bloco contíguo de memória.

f3lix
fonte
Acho que você está certo, vou dar uma olhada no NSCoding (a menos que você tenha um linky recomendado), os próprios objetos são feitos em NSStrings, isso é tudo, então suponho que poderia simplesmente escrever meu próprio serializador e colocá-los todos em uma matriz de string
Anthony Main
Ok, então implementei o NSCode, veja acima, mas ainda não tenho prazer em usar o NSArchiver
Anthony Main
0

Eu não recomendaria tentar armazenar coisas como essa no banco de dados padrão.

SQLite é bastante fácil de aprender e usar. Eu tenho um episódio em um dos meus screencasts ( http://pragprog.com/screencasts/v-bdiphone ) sobre um invólucro simples que escrevi (você pode obter o código sem comprar o SC).

É muito mais limpo armazenar dados de aplicativos no espaço de aplicativos.

Dito isso, se ainda faz sentido colocar esses dados no banco de dados padrão, consulte o post f3lix postado.

Bill Dudney
fonte
1
Obrigado pela sugestão, mas eu realmente não quero ter que começar a escrever consultas SQL (como vejo em seu exemplo) apenas para um objeto com algumas strings
Anthony Main