Como armazenar objetos personalizados em NSUserDefaults

268

Tudo bem, então eu ando bisbilhotando e percebo o meu problema, mas não sei como consertá-lo. Eu criei uma classe personalizada para armazenar alguns dados. Eu faço objetos para essa classe e preciso que eles durem entre as sessões. Antes de colocar todas as minhas informações NSUserDefaults, mas isso não está funcionando.

-[NSUserDefaults setObject:forKey:]: Attempt to insert non-property value '<Player: 0x3b0cc90>' of class 'Player'.

Essa é a mensagem de erro que recebo quando coloco minha classe personalizada, "Player", no NSUserDefaults. Agora, li que aparentemente NSUserDefaultsapenas armazena alguns tipos de informações. Então, como eu coloco meus objetos NSUSerDefaults?

Eu li que deveria haver uma maneira de "codificar" meu objeto personalizado e depois colocá-lo, mas não sei como implementá-lo, a ajuda seria apreciada! Obrigado!

****EDITAR****

Tudo bem, então trabalhei com o código fornecido abaixo (obrigado!), Mas ainda estou tendo alguns problemas. Basicamente, o código falha agora e não sei por que, porque não dá nenhum erro. Talvez esteja sentindo falta de algo básico e esteja cansado demais, mas vamos ver. Aqui está a implementação da minha classe Custom, "Player":

@interface Player : NSObject {
    NSString *name;
    NSNumber *life;
    //Log of player's life
}
//Getting functions, return the info
- (NSString *)name;
- (int)life;


- (id)init;

//These are the setters
- (void)setName:(NSString *)input; //string
- (void)setLife:(NSNumber *)input; //number    

@end

Arquivo de implementação:

#import "Player.h"
@implementation Player
- (id)init {
    if (self = [super init]) {
        [self setName:@"Player Name"];
        [self setLife:[NSNumber numberWithInt:20]];
        [self setPsnCounters:[NSNumber numberWithInt:0]];
    }
    return self;
}

- (NSString *)name {return name;}
- (int)life {return [life intValue];}
- (void)setName:(NSString *)input {
    [input retain];
    if (name != nil) {
        [name release];
    }
    name = input;
}
- (void)setLife:(NSNumber *)input {
    [input retain];
    if (life != nil) {
        [life release];
    }
    life = input;
}
/* This code has been added to support encoding and decoding my objecst */

-(void)encodeWithCoder:(NSCoder *)encoder
{
    //Encode the properties of the object
    [encoder encodeObject:self.name forKey:@"name"];
    [encoder encodeObject:self.life forKey:@"life"];
}

-(id)initWithCoder:(NSCoder *)decoder
{
    self = [super init];
    if ( self != nil )
    {
        //decode the properties
        self.name = [decoder decodeObjectForKey:@"name"];
        self.life = [decoder decodeObjectForKey:@"life"];
    }
    return self;
}
-(void)dealloc {
    [name release];
    [life release];
    [super dealloc];
}
@end

Então essa é a minha aula, bem direta, eu sei que funciona na criação de meus objetos. Então, aqui estão as partes relevantes do arquivo AppDelegate (onde eu chamo as funções de criptografia e descriptografia):

@class MainViewController;

@interface MagicApp201AppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    MainViewController *mainViewController;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) MainViewController *mainViewController;

-(void)saveCustomObject:(Player *)obj;
-(Player *)loadCustomObjectWithKey:(NSString*)key;


@end

E então as partes importantes do arquivo de implementação:

    #import "MagicApp201AppDelegate.h"
    #import "MainViewController.h"
    #import "Player.h"

    @implementation MagicApp201AppDelegate


    @synthesize window;
    @synthesize mainViewController;


    - (void)applicationDidFinishLaunching:(UIApplication *)application {
    NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
        //First check to see if some things exist
        int startup = [prefs integerForKey:@"appHasLaunched"];
        if (startup == nil) {
//Make the single player 
        Player *singlePlayer = [[Player alloc] init];
        NSLog([[NSString alloc] initWithFormat:@"%@\n%d\n%d",[singlePlayer name], [singlePlayer life], [singlePlayer psnCounters]]); //  test
        //Encode the single player so it can be stored in UserDefaults
        id test = [MagicApp201AppDelegate new];
        [test saveCustomObject:singlePlayer];
        [test release];
}
[prefs synchronize];
}

-(void)saveCustomObject:(Player *)object
{ 
    NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
    NSData *myEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];
    [prefs setObject:myEncodedObject forKey:@"testing"];
}

-(Player *)loadCustomObjectWithKey:(NSString*)key
{
    NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
    NSData *myEncodedObject = [prefs objectForKey:key ];
    Player *obj = (Player *)[NSKeyedUnarchiver unarchiveObjectWithData: myEncodedObject];
    return obj;
}

Eeee, desculpe por todo o código. Apenas tentando ajudar. Basicamente, o aplicativo será iniciado e falhará imediatamente. Eu o reduzi à parte de criptografia do aplicativo, é onde ele falha, então estou fazendo algo errado, mas não sei ao certo o que. Ajuda seria apreciada novamente, obrigado!

(Ainda não descriptografei, pois ainda não comecei a criptografar.)

Ethan Mick
fonte
Você tem um rastreamento de pilha ou mais informações sobre a falha, como qual número de linha está causando a falha? Não estou vendo imediatamente nada de errado com o código, portanto, um ponto de partida seria útil.
chrissr
No exemplo acima, você usou encodeObject para armazenar self.life, que é um int. Você deve usar encodeInt.
broot 28/01

Respostas:

516

Na sua classe Player, implemente os dois métodos a seguir (substituindo chamadas para encodeObject por algo relevante para seu próprio objeto):

- (void)encodeWithCoder:(NSCoder *)encoder {
    //Encode properties, other class variables, etc
    [encoder encodeObject:self.question forKey:@"question"];
    [encoder encodeObject:self.categoryName forKey:@"category"];
    [encoder encodeObject:self.subCategoryName forKey:@"subcategory"];
}

- (id)initWithCoder:(NSCoder *)decoder {
    if((self = [super init])) {
        //decode properties, other class vars
        self.question = [decoder decodeObjectForKey:@"question"];
        self.categoryName = [decoder decodeObjectForKey:@"category"];
        self.subCategoryName = [decoder decodeObjectForKey:@"subcategory"];
    }
    return self;
}

Leitura e escrita de NSUserDefaults:

- (void)saveCustomObject:(MyObject *)object key:(NSString *)key {
    NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setObject:encodedObject forKey:key];
    [defaults synchronize];

}

- (MyObject *)loadCustomObjectWithKey:(NSString *)key {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSData *encodedObject = [defaults objectForKey:key];
    MyObject *object = [NSKeyedUnarchiver unarchiveObjectWithData:encodedObject];
    return object;
}

Código emprestado descaradamente de: saving class in nsuserdefaults

chrissr
fonte
Editou minha postagem acima para refletir minhas alterações.
Ethan Mick
@chrissr você tem um erro nos padrões do NSUserDefaults = [NSUserDefaults standardUserDefaults]; ... deve ser o padrão NSUserDefaults *.
Maggie #
2
O NSKeyedArchiver arrasa ... parece que ele desce automaticamente para objetos NSArray ou NSDictionary e codifica quaisquer objetos personalizados codificáveis.
BadPirate
2
Eu não estou sendo pedante, apenas uma pergunta genuína, não é contra as diretrizes das maçãs usar setters sintetizados, isto é, self.property nos métodos init?
Samhan Salahuddin
2
@chrissr Por favor, mude NSData *myEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];para NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];. Variáveis ​​não coincidem.
GoRoS
36

Eu crio uma biblioteca RMMapper ( https://github.com/roomorama/RMMapper ) para ajudar a salvar objetos personalizados nos NSUserDefaults de maneira mais fácil e conveniente, porque implementar encodeWithCoder e initWithCoder é super chato!

Para marcar uma classe como arquivável, basta usar: #import "NSObject+RMArchivable.h"

Para salvar um objeto personalizado no NSUserDefaults:

#import "NSUserDefaults+RMSaveCustomObject.h"
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
[defaults rm_setCustomObject:user forKey:@"SAVED_DATA"];

Para obter obj personalizado a partir de NSUserDefaults:

user = [defaults rm_customObjectForKey:@"SAVED_DATA"]; 
thomasdao
fonte
Sua biblioteca pressupõe que você possui propriedades para tudo o que deseja persistir e que deseja persistir tudo para o qual possui propriedades. Se isso for verdade, sua biblioteca certamente pode ser útil, mas acho que você descobrirá que, em muitos casos, não é.
Erik B
É útil persistir apenas em objetos simples, eu diria que seu uso é bastante semelhante ao Gson em Java. Para quais casos você está olhando?
Thomasdao 7/11
7
Isso foi realmente útil, feliz por eu continuar lendo as respostas; D
Albara
e quando meu objeto personalizado tiver na propriedade outro objeto personalizado?
Shial
@Shial: se você fizer outra arquivável objeto personalizado, ele será salvo muito
thomasdao
35

Swift 4

Introduziu o protocolo Codable , que faz toda a mágica para esse tipo de tarefa. Basta conformar sua estrutura / classe personalizada a ele:

struct Player: Codable {
  let name: String
  let life: Double
}

E para armazenar nos Padrões, você pode usar o PropertyListEncoder / Decoder :

let player = Player(name: "Jim", life: 3.14)
UserDefaults.standard.set(try! PropertyListEncoder().encode(player), forKey: kPlayerDefaultsKey)

let storedObject: Data = UserDefaults.standard.object(forKey: kPlayerDefaultsKey) as! Data
let storedPlayer: Player = try! PropertyListDecoder().decode(Player.self, from: storedObject)

Funcionará assim também para matrizes e outras classes de contêiner de tais objetos:

try! PropertyListDecoder().decode([Player].self, from: storedArray)
Sergiu Todirascu
fonte
1
Funciona como um encanto
Nathan Barreto
Note que você só obter o Codablecomportamento de graça, quando todos os membros de instância são eles próprios Codable- caso contrário, você tem que escrever um código de codificação mesmo,
BallpointBen
1
Ou melhor, simplesmente conformar esses tipos de membros Codabletambém. E assim por diante, recursivamente. Somente em casos muito personalizados, você precisaria escrever um código de codificação.
Sergiu Todirascu
A maneira mais fácil com Swift 4.
alstr
31

Se alguém estiver procurando por uma versão rápida:

1) Crie uma classe personalizada para seus dados

class customData: NSObject, NSCoding {
let name : String
let url : String
let desc : String

init(tuple : (String,String,String)){
    self.name = tuple.0
    self.url = tuple.1
    self.desc = tuple.2
}
func getName() -> String {
    return name
}
func getURL() -> String{
    return url
}
func getDescription() -> String {
    return desc
}
func getTuple() -> (String,String,String) {
    return (self.name,self.url,self.desc)
}

required init(coder aDecoder: NSCoder) {
    self.name = aDecoder.decodeObjectForKey("name") as! String
    self.url = aDecoder.decodeObjectForKey("url") as! String
    self.desc = aDecoder.decodeObjectForKey("desc") as! String
}

func encodeWithCoder(aCoder: NSCoder) {
    aCoder.encodeObject(self.name, forKey: "name")
    aCoder.encodeObject(self.url, forKey: "url")
    aCoder.encodeObject(self.desc, forKey: "desc")
} 
}

2) Para salvar os dados, use a seguinte função:

func saveData()
    {
        let data  = NSKeyedArchiver.archivedDataWithRootObject(custom)
        let defaults = NSUserDefaults.standardUserDefaults()
        defaults.setObject(data, forKey:"customArray" )
    }

3) Para recuperar:

if let data = NSUserDefaults.standardUserDefaults().objectForKey("customArray") as? NSData
        {
             custom = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! [customData]
        }

Nota: Aqui estou salvando e recuperando uma matriz dos objetos de classe personalizada.

Ankit Goel
fonte
9

Tomando a resposta do @ chrissr e rodando com ele, esse código pode ser implementado em uma boa categoria NSUserDefaultspara salvar e recuperar objetos personalizados:

@interface NSUserDefaults (NSUserDefaultsExtensions)

- (void)saveCustomObject:(id<NSCoding>)object
                     key:(NSString *)key;
- (id<NSCoding>)loadCustomObjectWithKey:(NSString *)key;

@end


@implementation NSUserDefaults (NSUserDefaultsExtensions)


- (void)saveCustomObject:(id<NSCoding>)object
                     key:(NSString *)key {
    NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];
    [self setObject:encodedObject forKey:key];
    [self synchronize];

}

- (id<NSCoding>)loadCustomObjectWithKey:(NSString *)key {
    NSData *encodedObject = [self objectForKey:key];
    id<NSCoding> object = [NSKeyedUnarchiver unarchiveObjectWithData:encodedObject];
    return object;
}

@end

Uso:

[[NSUserDefaults standardUserDefaults] saveCustomObject:myObject key:@"myKey"];
Carlos P
fonte
7

Swift 3

class MyObject: NSObject, NSCoding  {
    let name : String
    let url : String
    let desc : String

    init(tuple : (String,String,String)){
        self.name = tuple.0
        self.url = tuple.1
        self.desc = tuple.2
    }
    func getName() -> String {
        return name
    }
    func getURL() -> String{
        return url
    }
    func getDescription() -> String {
        return desc
    }
    func getTuple() -> (String, String, String) {
        return (self.name,self.url,self.desc)
    }

    required init(coder aDecoder: NSCoder) {
        self.name = aDecoder.decodeObject(forKey: "name") as? String ?? ""
        self.url = aDecoder.decodeObject(forKey: "url") as? String ?? ""
        self.desc = aDecoder.decodeObject(forKey: "desc") as? String ?? ""
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(self.name, forKey: "name")
        aCoder.encode(self.url, forKey: "url")
        aCoder.encode(self.desc, forKey: "desc")
    }
    }

para armazenar e recuperar:

func save() {
            let data  = NSKeyedArchiver.archivedData(withRootObject: object)
            UserDefaults.standard.set(data, forKey:"customData" )
        }
        func get() -> MyObject? {
            guard let data = UserDefaults.standard.object(forKey: "customData") as? Data else { return nil }
            return NSKeyedUnarchiver.unarchiveObject(with: data) as? MyObject
        }
Vyacheslav
fonte
Apenas um lembrete: selfnão é obrigatório antes das variáveis ​​em inite encode.
Tamás Sengel
Você pode mostrar como você está intializing objeto com NSCoder
Abhishek Thapliyal
@AbhishekThapliyal, eu não entendo você. init (codificador aDecoder: NSCoder) inicializa
Vyacheslav
6

Sincronize os dados / objeto que você salvou nos NSUserDefaults

-(void)saveCustomObject:(Player *)object
{ 
    NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
    NSData *myEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];
    [prefs setObject:myEncodedObject forKey:@"testing"];
    [prefs synchronize];
}

Espero que isso ajude você. obrigado

Muhammad Aamir Ali
fonte
Observe as pessoas no futuro que, no Swift 3, "sincronizar" não deve ser chamado por você.
Jose Ramirez
@JozemiteApps why? ou você pode postar um link com explicação para este tópico?
precisa saber é o seguinte
@ code4latte Não sei se ele foi alterado, mas a documentação da Apple afirma que "o método synchronize (), que é automaticamente chamado a intervalos periódicos, mantém o cache da memória sincronizado com o banco de dados de padrões do usuário". Antes, dizia que você deveria chamá-lo apenas se tiver certeza de que precisa dos dados salvos instantaneamente. Li algumas vezes que o usuário não deveria mais chamá-lo.
Jose Ramirez