Cómo almacenar objetos personalizados en NSUserDefaults

268

Muy bien, he estado hurgando un poco y me doy cuenta de mi problema, pero no sé cómo solucionarlo. He hecho una clase personalizada para contener algunos datos. Hago objetos para esta clase, y necesito que duren entre sesiones. Antes de poner toda mi información NSUserDefaults, pero esto no funciona.

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

Ese es el mensaje de error que recibo cuando pongo mi clase personalizada, "Player", en el NSUserDefaults. Ahora, he leído que aparentemente NSUserDefaultssolo almacena algunos tipos de información. Entonces, ¿cómo puedo obtener mis objetos NSUSerDefaults?

Leí que debería haber una forma de "codificar" mi objeto personalizado y luego colocarlo, pero no estoy seguro de cómo implementarlo, ¡se agradecería su ayuda! ¡Gracias!

****EDITAR****

Muy bien, así que trabajé con el código que figura a continuación (¡Gracias!), Pero todavía tengo algunos problemas. Básicamente, el código se bloquea ahora y no estoy seguro de por qué, porque no da ningún error. Quizás me estoy perdiendo algo básico y estoy demasiado cansado, pero ya veremos. Aquí está la implementación de mi clase personalizada, "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

Archivo de Implementación:

#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

Esa es mi clase, bastante directa, sé que funciona para hacer mis objetos. Aquí están las partes relevantes del archivo AppDelegate (donde llamo a las funciones de cifrado y descifrado):

@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

Y luego las partes importantes del archivo de implementación:

    #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, perdón por todo el código. Sólo trato de ayudar. Básicamente, la aplicación se iniciará y luego se bloqueará de inmediato. Lo reduje a la parte de cifrado de la aplicación, ahí es donde se bloquea, así que estoy haciendo algo mal pero no estoy seguro de qué. La ayuda sería apreciada nuevamente, ¡gracias!

(Todavía no he podido descifrar, ya que todavía no he logrado el cifrado).

Ethan Mick
fuente
¿Tiene un seguimiento de la pila o más información sobre el bloqueo, como qué número de línea está causando el bloqueo? No veo inmediatamente nada malo en el código, por lo que sería útil un punto de partida.
chrissr
En el ejemplo anterior, ha utilizado encodeObject para almacenar self.life, que es un int. Debería usar encodeInt en su lugar.
broot

Respuestas:

516

En su clase Player, implemente los siguientes dos métodos (sustituyendo llamadas a encodeObject con algo relevante para su propio 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;
}

Lectura y escritura 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 tomado descaradamente de: guardar la clase en nsuserdefaults

chrissr
fuente
Edité mi publicación anterior para reflejar mis cambios.
Ethan Mick
@chrissr tiene un error en NSUserDefaults defaults = [NSUserDefaults standardUserDefaults]; ... debe ser NSUserDefaults * por defecto.
Maggie
2
NSKeyedArchiver oscila ... parece que incluso desciende automáticamente a objetos NSArray o NSDictionary y codifica cualquier objeto personalizado codificable dentro.
BadPirate
2
No estoy siendo pedante, solo una pregunta genuina, ¿no va en contra de las pautas de las manzanas usar setters sintetizados, es decir, self.property en los métodos init?
Samhan Salahuddin
2
@chrissr Cambia NSData *myEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];por NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];. Las variables no coinciden.
GoRoS
36

Creo una biblioteca RMMapper ( https://github.com/roomorama/RMMapper ) para ayudar a guardar objetos personalizados en NSUserDefaults de manera más fácil y conveniente, ¡porque implementar encodeWithCoder e initWithCoder es súper aburrido!

Para marcar una clase como archivable, solo use: #import "NSObject+RMArchivable.h"

Para guardar un objeto personalizado en NSUserDefaults:

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

Para obtener obj personalizado de NSUserDefaults:

user = [defaults rm_customObjectForKey:@"SAVED_DATA"]; 
thomasdao
fuente
Su biblioteca asume que tiene propiedades para todo lo que desea persistir y que desea conservar todas las propiedades para las que tiene. Si esto es cierto, su biblioteca ciertamente podría ser útil, pero creo que encontrará que en muchos casos no lo es.
Erik B
Es útil para persistir solo objetos simples, diría que su uso es bastante similar al de Gson en Java. ¿Qué casos estás viendo?
thomasdao
77
Eso fue realmente útil, me alegro de haber seguido leyendo sobre las respuestas; D
Albara
¿Qué pasa cuando mi objeto personalizado tiene en propiedad otro objeto personalizado?
Shial
@Shial: si hace que se pueda archivar otro objeto personalizado, también se guardará
thomasdao
35

Swift 4

Introdujo el protocolo Codificable que hace toda la magia para este tipo de tareas. Simplemente conforma tu estructura / clase personalizada:

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

Y para almacenar los valores predeterminados, puede usar el 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á así para las matrices y otras clases de contenedor de tales objetos también:

try! PropertyListDecoder().decode([Player].self, from: storedArray)
Sergiu Todirascu
fuente
1
Funciona como un encanto
Nathan Barreto
Tenga en cuenta que sólo se obtiene el Codablecomportamiento de forma gratuita cuando todos los miembros de instancia son ellos mismos Codable- de lo contrario, usted tiene que escribir un código de codificación ti mismo,
BallpointBen
1
O, más bien, simplemente conforma esos tipos de miembros Codabletambién. Y así sucesivamente, recursivamente. Solo en casos muy personalizados necesitaría escribir código de codificación.
Sergiu Todirascu
La forma más fácil con Swift 4.
alstr
31

Si alguien está buscando una versión rápida:

1) Crea una clase personalizada para tus datos

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 guardar datos use la siguiente función:

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: Aquí estoy guardando y recuperando una matriz de los objetos de clase personalizados.

Ankit Goel
fuente
9

Tomando la respuesta de @ chrissr y ejecutándola, este código se puede implementar en una buena categoría NSUserDefaultspara guardar y 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
fuente
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 almacenar y 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
fuente
Solo un recordatorio: selfno es obligatorio antes de las variables en inity encode.
Tamás Sengel
¿Puede mostrar cómo está iniciando un objeto con NSCoder
Abhishek Thapliyal
@AbhishekThapliyal, no te entiendo. init (codificador aDecoder: NSCoder) lo inicializa
Vyacheslav
6

Sincronice los datos / objetos que ha guardado en NSUserDefaults

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

Espero que esto te ayudará. Gracias

Muhammad Aamir Ali
fuente
Tenga en cuenta a las personas del futuro que a partir de Swift 3, "sincronizar" no debe ser llamado por usted.
José Ramírez
@JozemiteApps ¿por qué? o puedes publicar un enlace con una explicación para este tema?
code4latte
@ code4latte No sé si se cambió, pero la documentación de Apple dice que "El método syncize (), que se invoca automáticamente a intervalos periódicos, mantiene la caché en memoria sincronizada con la base de datos predeterminada de un usuario". Antes, decía que solo debería llamarlo SI está seguro de que necesita guardar los datos al instante. Leí un par de veces que el usuario ya no debería llamarlo.
Jose Ramirez