Comment stocker des objets personnalisés dans NSUserDefaults

268

D'accord, j'ai donc fait quelques fouilles et je me rends compte de mon problème, mais je ne sais pas comment le résoudre. J'ai créé une classe personnalisée pour contenir certaines données. Je fabrique des objets pour cette classe et j'en ai besoin pour durer entre les sessions. Avant de mettre toutes mes informations NSUserDefaults, mais cela ne fonctionne pas.

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

C'est le message d'erreur que j'obtiens lorsque je mets ma classe personnalisée, "Player", dans le NSUserDefaults. Maintenant, j'ai lu que, apparemment, NSUserDefaultsne stocke que certains types d'informations. Alors, comment puis-je obtenir mes objets NSUSerDefaults?

J'ai lu qu'il devrait y avoir un moyen de "coder" mon objet personnalisé puis de le mettre, mais je ne sais pas comment l'implémenter, une aide serait appréciée! Je vous remercie!

****ÉDITER****

D'accord, j'ai donc travaillé avec le code donné ci-dessous (merci!), Mais j'ai toujours des problèmes. Fondamentalement, le code se bloque maintenant et je ne sais pas pourquoi, car il ne donne aucune erreur. Peut-être que je manque quelque chose de basique et que je suis juste trop fatigué, mais nous verrons. Voici l'implémentation de ma 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

Fichier d'implémentation:

#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

Voilà donc ma classe, assez simple, je sais que cela fonctionne dans la fabrication de mes objets. Voici donc les parties pertinentes du fichier AppDelegate (où j'appelle les fonctions de cryptage et de décryptage):

@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

Et puis les parties importantes du fichier d'implémentation:

    #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, désolé pour tout le code. J'essaye juste d'aider. Fondamentalement, l'application se lance et se bloque immédiatement. Je l'ai réduit à la partie cryptage de l'application, c'est là que ça se bloque, donc je fais quelque chose de mal mais je ne sais pas quoi. De l'aide serait encore appréciée, merci!

(Je n'ai pas encore commencé à déchiffrer, car je n'ai pas encore réussi à chiffrer.)

Ethan Mick
la source
Avez-vous une trace de pile ou plus d'informations sur le crash, telles que le numéro de ligne à l'origine du crash? Je ne vois immédiatement rien de mal avec le code, donc un point de départ serait utile.
chrissr
Dans l'exemple ci-dessus, vous avez utilisé encodeObject pour stocker self.life qui est un int. Vous devez utiliser encodeInt à la place.
broot

Réponses:

516

Sur votre classe Player, implémentez les deux méthodes suivantes (en remplaçant les appels à encodeObject par quelque chose de pertinent pour votre propre objet):

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

Lecture et écriture 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;
}

Code emprunté sans vergogne à: sauvegarde de classe dans nsuserdefaults

chrissr
la source
Modifié mon message ci-dessus pour refléter mes changements.
Ethan Mick
@chrissr vous avez une erreur dans NSUserDefaults defaults = [NSUserDefaults standardUserDefaults]; ... devrait être NSUserDefaults * par défaut.
Maggie
2
NSKeyedArchiver bascule ... semble même descendre automatiquement dans les objets NSArray ou NSDictionary et encoder tous les objets personnalisés encodables à l'intérieur.
BadPirate
2
Je ne suis pas pédant, juste une vraie question, n'est-ce pas contre les directives des pommes d'utiliser des setters synthétisés, c'est-à-dire self.property dans les méthodes init?
Samhan Salahuddin
2
@chrissr Veuillez changer NSData *myEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];pour NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];. Les variables ne correspondent pas.
GoRoS
36

Je crée une bibliothèque RMMapper ( https://github.com/roomorama/RMMapper ) pour aider à enregistrer un objet personnalisé dans NSUserDefaults plus facilement et plus facilement, car la mise en œuvre d'encodeWithCoder et initWithCoder est super ennuyeuse!

Pour marquer une classe comme archivable, utilisez simplement: #import "NSObject+RMArchivable.h"

Pour enregistrer un objet personnalisé dans NSUserDefaults:

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

Pour obtenir un obj personnalisé à partir de NSUserDefaults:

user = [defaults rm_customObjectForKey:@"SAVED_DATA"]; 
thomasdao
la source
Votre bibliothèque suppose que vous avez des propriétés pour tout ce que vous souhaitez conserver et que vous souhaitez conserver tout ce pour quoi vous avez des propriétés. Si cela est vrai, votre bibliothèque pourrait certainement être utile, mais je pense que vous constaterez que ce n'est pas souvent le cas.
Erik B
Il est utile de conserver un objet simple uniquement, je dirais que son utilisation est assez similaire à Gson en Java. Quels cas regardez-vous?
thomasdao
7
Cela a été très utile, heureux d'avoir continué à lire sur les réponses; D
Albara
Qu'en est-il lorsque mon objet personnalisé a dans la propriété un autre objet personnalisé?
Shial
@Shial: si vous rendez un autre objet personnalisé archivable, il sera également enregistré
thomasdao
35

Swift 4

Introduit le protocole Codable qui fait toute la magie pour ce genre de tâches. Il vous suffit de lui conformer votre structure / classe personnalisée:

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

Et pour stocker dans les valeurs par défaut, vous pouvez utiliser le 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)

Cela fonctionnera comme ça pour les tableaux et autres classes de conteneurs de ces objets aussi:

try! PropertyListDecoder().decode([Player].self, from: storedArray)
Sergiu Todirascu
la source
1
Fonctionne comme un charme
Nathan Barreto
Notez que vous obtenez le Codablecomportement gratuitement uniquement lorsque tous les membres de l'instance sont eux Codable- mêmes - sinon, vous devez écrire vous-même du code d'encodage,
BallpointBen
1
Ou plutôt, conformez simplement ces types de membres Codable. Et ainsi de suite, récursivement. Seulement dans des cas très personnalisés, vous devrez écrire du code d'encodage.
Sergiu Todirascu
Le moyen le plus simple avec Swift 4.
alstr
31

Si quelqu'un cherche une version rapide:

1) Créez une classe personnalisée pour vos données

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) Pour enregistrer les données, utilisez la fonction suivante:

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

3) Pour récupérer:

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

Remarque: Ici, je sauvegarde et récupère un tableau des objets de classe personnalisés.

Ankit Goel
la source
9

En prenant la réponse de @ chrissr et en l'exécutant, ce code peut être implémenté dans une belle catégorie NSUserDefaultspour enregistrer et récupérer des objets personnalisés:

@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

Usage:

[[NSUserDefaults standardUserDefaults] saveCustomObject:myObject key:@"myKey"];
Carlos P
la source
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")
    }
    }

pour stocker et récupérer:

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
la source
Juste un rappel: selfn'est pas obligatoire avant les variables dans initet encode.
Tamás Sengel
Pouvez-vous montrer comment vous initialisez un objet avec NSCoder
Abhishek Thapliyal
@AbhishekThapliyal, je ne vous comprends pas. init (codeur aDecoder: NSCoder) l'initialise
Vyacheslav
6

Synchronisez les données / objets que vous avez enregistrés dans NSUserDefaults

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

J'espère que ceci vous aidera. Merci

Muhammad Aamir Ali
la source
Notez que les gens du futur à partir de Swift 3, "synchroniser" ne devraient plus du tout être appelés par vous.
Jose Ramirez
@JozemiteApps pourquoi? ou pouvez-vous publier un lien avec des explications sur ce sujet?
code4latte
@ code4latte Je ne sais pas si elle a été modifiée, mais la documentation d'Apple indique que "La méthode synchronize (), qui est automatiquement invoquée à intervalles périodiques, maintient le cache en mémoire synchronisé avec la base de données par défaut d'un utilisateur." Avant, il était dit que vous ne devriez l'appeler que si vous êtes sûr d'avoir besoin des données enregistrées instantanément. J'ai lu quelques fois que l'utilisateur ne devrait plus l'appeler.
Jose Ramirez