Speichern benutzerdefinierter Objekte in NSUserDefaults

268

Okay, also habe ich ein bisschen herumgestochert und ich erkenne mein Problem, aber ich weiß nicht, wie ich es beheben soll. Ich habe eine benutzerdefinierte Klasse erstellt, um einige Daten zu speichern. Ich mache Objekte für diese Klasse und ich muss sie zwischen den Sitzungen halten. Vorher habe ich alle meine Informationen eingegeben NSUserDefaults, aber das funktioniert nicht.

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

Dies ist die Fehlermeldung, die ich erhalte, wenn ich meine benutzerdefinierte Klasse "Player" in die NSUserDefaults. Jetzt habe ich gelesen, dass anscheinend NSUserDefaultsnur einige Arten von Informationen gespeichert werden. Wie bekomme ich meine Objekte hinein NSUSerDefaults?

Ich habe gelesen, dass es eine Möglichkeit geben sollte, mein benutzerdefiniertes Objekt zu "codieren" und dann einzufügen, aber ich bin mir nicht sicher, wie ich es implementieren soll. Hilfe wäre willkommen! Danke dir!

****BEARBEITEN****

Okay, also habe ich mit dem unten angegebenen Code gearbeitet (Danke!), Aber ich habe immer noch einige Probleme. Grundsätzlich stürzt der Code jetzt ab und ich bin mir nicht sicher warum, weil er keine Fehler enthält. Vielleicht fehlt mir etwas Grundlegendes und ich bin einfach zu müde, aber wir werden sehen. Hier ist die Implementierung meiner benutzerdefinierten Klasse "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

Implementierungsdatei:

#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

Das ist also meine Klasse, ziemlich einfach, ich weiß, dass es bei der Herstellung meiner Objekte funktioniert. Hier sind die relevanten Teile der AppDelegate-Datei (wo ich die Verschlüsselungs- und Entschlüsselungsfunktionen aufrufe):

@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

Und dann die wichtigen Teile der Implementierungsdatei:

    #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, entschuldige den ganzen Code. Ich versuche nur zu helfen. Grundsätzlich wird die App gestartet und stürzt dann sofort ab. Ich habe es auf den Verschlüsselungsteil der App eingegrenzt. Dort stürzt es ab. Ich mache also etwas falsch, bin mir aber nicht sicher, was. Hilfe wäre wieder dankbar, danke!

(Ich bin noch nicht zum Entschlüsseln gekommen, da ich noch nicht zum Verschlüsseln gekommen bin.)

Ethan Mick
quelle
Haben Sie eine Stapelverfolgung oder weitere Informationen zum Absturz, z. B. welche Zeilennummer den Absturz verursacht? Ich sehe nicht sofort etwas falsch mit dem Code, daher wäre ein Ausgangspunkt hilfreich.
Chrissr
Im obigen Beispiel haben Sie encodeObject verwendet, um self.life zu speichern, das ein int ist. Sie sollten stattdessen encodeInt verwenden.
Brut

Antworten:

516

Implementieren Sie in Ihrer Player-Klasse die folgenden zwei Methoden (Ersetzen von Aufrufen von encodeObject durch etwas, das für Ihr eigenes Objekt relevant ist):

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

Lesen und Schreiben von 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 schamlos entlehnt von: Speichern der Klasse in nsuserdefaults

chrissr
quelle
Ich habe meinen Beitrag oben bearbeitet, um meine Änderungen widerzuspiegeln.
Ethan Mick
@chrissr Sie haben einen Fehler in NSUserDefaults defaults = [NSUserDefaults standardUserDefaults]; ... sollte NSUserDefaults * Standardeinstellungen sein.
Maggie
2
NSKeyedArchiver rockt ... scheint sogar automatisch in NSArray- oder NSDictionary-Objekte abzusteigen und alle darin codierbaren benutzerdefinierten Objekte zu codieren.
BadPirate
2
Ich bin nicht pedantisch, nur eine echte Frage. Verstößt es nicht gegen die Richtlinien von Äpfeln, synthetisierte Setter zu verwenden, dh self.property in init-Methoden?
Samhan Salahuddin
2
@chrissr Bitte ändern NSData *myEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];für NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];. Variablen stimmen nicht überein.
GoRoS
36

Ich erstelle eine Bibliothek RMMapper ( https://github.com/roomorama/RMMapper ), um das Speichern von benutzerdefinierten Objekten in NSUserDefaults einfacher und bequemer zu machen, da die Implementierung von encodeWithCoder und initWithCoder sehr langweilig ist!

Um eine Klasse als archivierbar zu markieren, verwenden Sie einfach: #import "NSObject+RMArchivable.h"

So speichern Sie ein benutzerdefiniertes Objekt in NSUserDefaults:

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

So erhalten Sie ein benutzerdefiniertes Objekt von NSUserDefaults:

user = [defaults rm_customObjectForKey:@"SAVED_DATA"]; 
thomasdao
quelle
Ihre Bibliothek geht davon aus, dass Sie Eigenschaften für alles haben, was Sie beibehalten möchten, und dass Sie alles beibehalten möchten, für das Sie Eigenschaften haben. Wenn dies zutrifft, könnte Ihre Bibliothek sicherlich hilfreich sein, aber ich denke, Sie werden feststellen, dass dies in vielen Fällen nicht der Fall ist.
Erik B
Es ist nützlich, nur einfache Objekte beizubehalten. Ich würde sagen, dass seine Verwendung Gson in Java ziemlich ähnlich ist. Welche Fälle sehen Sie?
Thomasdao
7
Das war wirklich hilfreich, ich bin froh, dass ich die Antworten weiter gelesen habe; D
Albara
Was ist, wenn mein benutzerdefiniertes Objekt in der Eigenschaft ein anderes benutzerdefiniertes Objekt hat?
Shial
@ Shial: Wenn Sie andere benutzerdefinierte Objekte archivierbar machen, wird es auch gespeichert
Thomasdao
35

Swift 4

Einführung des codierbaren Protokolls, das die ganze Magie für diese Art von Aufgaben ausübt . Passen Sie einfach Ihre benutzerdefinierte Struktur / Klasse daran an:

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

Zum Speichern in den Standardeinstellungen können Sie den PropertyListEncoder / Decoder verwenden :

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)

Dies funktioniert auch für Arrays und andere Containerklassen solcher Objekte:

try! PropertyListDecoder().decode([Player].self, from: storedArray)
Sergiu Todirascu
quelle
1
Funktioniert wie ein Zauber
Nathan Barreto
Beachten Sie, dass Sie das CodableVerhalten nur dann kostenlos erhalten, wenn alle Instanzmitglieder selbst sind Codable- andernfalls müssen Sie selbst einen Codierungscode schreiben
BallpointBen
1
Oder passen Sie diese Elementtypen einfach an Codable. Und so weiter rekursiv. Nur in sehr benutzerdefinierten Fällen müssten Sie Codierungscode schreiben.
Sergiu Todirascu
Der einfachste Weg mit Swift 4.
Alstr
31

Wenn jemand nach einer schnellen Version sucht:

1) Erstellen Sie eine benutzerdefinierte Klasse für Ihre Daten

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) Um Daten zu speichern, verwenden Sie folgende Funktion:

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

3) So rufen Sie ab:

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

Hinweis: Hier speichere und rufe ich ein Array der benutzerdefinierten Klassenobjekte ab.

Ankit Goel
quelle
9

Wenn Sie die Antwort von @ chrissr nehmen und damit ausführen, kann dieser Code in eine nette Kategorie implementiert werden NSUserDefaults, um benutzerdefinierte Objekte zu speichern und abzurufen:

@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

Verwendungszweck:

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

zum Speichern und Abrufen:

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
quelle
Nur zur Erinnerung: selfist vor den Variablen in initund nicht obligatorisch encode.
Tamás Sengel
Können Sie zeigen, wie Sie Objekt mit NSCoder
Abhishek Thapliyal
@AbhishekThapliyal, ich verstehe dich nicht. init (Codierer aDecoder: NSCoder) initialisiert es
Vyacheslav
6

Synchronisieren Sie die Daten / Objekte, die Sie in NSUserDefaults gespeichert haben

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

Hoffe das wird dir helfen. Vielen Dank

Muhammad Aamir Ali
quelle
Beachten Sie Leute aus der Zukunft, dass ab Swift 3 "Synchronisieren" von Ihnen überhaupt nicht mehr aufgerufen werden sollte.
Jose Ramirez
@JozemiteApps warum? oder kannst du einen Link mit Erklärungen zu diesem Thema posten?
Code4latte
@ code4latte Ich weiß nicht, ob es geändert wurde, aber in der Dokumentation von Apple heißt es: "Die synchronize () -Methode, die in regelmäßigen Abständen automatisch aufgerufen wird, hält den speicherinternen Cache mit der Standarddatenbank eines Benutzers synchron." Zuvor hieß es, Sie sollten es nur aufrufen, wenn Sie sicher sind, dass die Daten sofort gespeichert werden müssen. Ich habe ein paar Mal gelesen, dass der Benutzer es nicht mehr anrufen sollte.
Jose Ramirez