Jak przechowywać niestandardowe obiekty w NSUserDefaults

268

W porządku, więc trochę się grzebałem i zdaję sobie sprawę z mojego problemu, ale nie wiem, jak to naprawić. Stworzyłem niestandardową klasę do przechowywania niektórych danych. Tworzę przedmioty dla tej klasy i muszę je trwać między sesjami. Przed wprowadzeniem wszystkich moich informacji NSUserDefaults, ale to nie działa.

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

To jest komunikat o błędzie, który pojawia się, gdy umieszczam swoją niestandardową klasę „Player” w NSUserDefaults. Przeczytałem, że najwyraźniej NSUserDefaultsprzechowuje tylko niektóre rodzaje informacji. Jak mam wprowadzić moje przedmioty NSUSerDefaults?

Przeczytałem, że powinien istnieć sposób na „zakodowanie” mojego niestandardowego obiektu, a następnie wprowadzenie go, ale nie jestem pewien, jak go zaimplementować, pomoc byłaby mile widziana! Dziękuję Ci!

****EDYTOWAĆ****

W porządku, więc pracowałem z kodem podanym poniżej (Dziękuję!), Ale wciąż mam problemy. Zasadniczo kod ulega awarii teraz i nie jestem pewien, dlaczego, ponieważ nie powoduje żadnych błędów. Może brakuje mi czegoś podstawowego i jestem po prostu zbyt zmęczony, ale zobaczymy. Oto implementacja mojej klasy niestandardowej „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

Plik implementacyjny:

#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

Więc to moja klasa, całkiem prosto, wiem, że to działa w tworzeniu moich obiektów. Oto odpowiednie części pliku AppDelegate (gdzie nazywam funkcje szyfrowania i deszyfrowania):

@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

A następnie ważne części pliku implementacji:

    #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, przepraszam za cały kod. Po prostu próbuje pomóc. Zasadniczo aplikacja zostanie uruchomiona, a następnie natychmiast się zawiesi. Zawęziłem to do części szyfrującej aplikacji, tam właśnie się zawiesza, więc robię coś złego, ale nie jestem pewien, co. Pomoc będzie mile widziana, dziękuję!

(Nie udało mi się jeszcze odszyfrować, ponieważ szyfrowanie jeszcze nie działa).

Ethan Mick
źródło
Czy masz ślad stosu lub więcej informacji o awarii, takich jak numer linii, która powoduje awarię? Nie widzę od razu nic złego w kodzie, więc punkt początkowy byłby pomocny.
chrissr
W powyższym przykładzie użyłeś encodeObject do przechowywania self.life, które jest int. Zamiast tego powinieneś użyć encodeInt.
Broro

Odpowiedzi:

516

W klasie Player zaimplementuj następujące dwie metody (zastępując wywołania encodeObject czymś istotnym dla twojego obiektu):

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

Czytanie i pisanie z 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;
}

Kod bezwstydnie zapożyczony z: zapisywanie klasy w nsuserdefaults

chrissr
źródło
Edytowałem mój post powyżej, aby odzwierciedlić moje zmiany.
Ethan Mick
@ chrissr masz błąd w NSUserDefaults defaults = [NSUserDefaults standardUserDefaults]; ... powinny mieć wartości domyślne NSUserDefaults *.
Maggie,
2
Skały NSKeyedArchiver ... wydaje się, że nawet automatycznie schodzi do obiektów NSArray lub NSDictionary i koduje wszelkie niestandardowe obiekty, które można kodować.
BadPirate
2
Nie jestem pedantyczny, tylko prawdziwe pytanie, czy nie jest sprzeczne z wytycznymi dotyczącymi jabłek stosowanie syntetyzowanych ustawień, tj. Self.property w metodach init?
Samhan Salahuddin
2
@chrissr Proszę zmienić NSData *myEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];na NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];. Zmienne nie pasują.
GoRoS
36

Tworzę bibliotekę RMMapper ( https://github.com/roomorama/RMMapper ), aby łatwiej i wygodniej zapisywać niestandardowy obiekt w NSUserDefault, ponieważ implementacja encodeWithCoder i initWithCoder jest bardzo nudna!

Aby oznaczyć klasę jako archiwalną, wystarczy użyć: #import "NSObject+RMArchivable.h"

Aby zapisać obiekt niestandardowy w NSUserDefaults:

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

Aby uzyskać niestandardowy obiekt z NSUserDefaults:

user = [defaults rm_customObjectForKey:@"SAVED_DATA"]; 
thomasdao
źródło
Twoja biblioteka zakłada, że ​​masz właściwości wszystkiego, co chcesz utrwalić i że chcesz utrwalić wszystko, co masz właściwości. Jeśli to prawda, twoja biblioteka z pewnością może być pomocna, ale myślę, że przekonasz się, że w wielu przypadkach tak nie jest.
Erik B,
Przydatne jest utrzymywanie tylko zwykłego obiektu, powiedziałbym, że jego użycie jest bardzo podobne do Gsona w Javie. Na jakie przypadki patrzysz?
thomasdao,
7
To było bardzo pomocne, cieszę się, że wciąż czytam odpowiedzi; D
Albara,
co jeśli mój obiekt niestandardowy ma we właściwości inny obiekt niestandardowy?
Shial
@Shial: jeśli umożliwisz archiwizację innego obiektu niestandardowego, zostanie on również zapisany
thomasdao
35

Szybki 4

Wprowadzono protokół Codable , który wykonuje całą magię dla tego rodzaju zadań. Po prostu dostosuj do tego swoją własną strukturę / klasę:

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

A do przechowywania w ustawieniach domyślnych możesz użyć 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)

Będzie to działać tak samo w przypadku tablic i innych klas kontenerów takich obiektów:

try! PropertyListDecoder().decode([Player].self, from: storedArray)
Sergiu Todirascu
źródło
1
Działa jak urok
Nathan Barreto
Należy pamiętać, że można dostać tylko Codablezachowanie za darmo, gdy wszyscy członkowie instancji są sami Codable- w przeciwnym razie, trzeba napisać kod kodowanie siebie,
BallpointBen
1
A raczej po prostu dostosuj również te typy elementów Codable. I tak dalej, rekurencyjnie. Tylko w bardzo niestandardowych przypadkach trzeba byłoby napisać kodowanie.
Sergiu Todirascu
Najłatwiejszy sposób dzięki Swift 4.
alstr
31

Jeśli ktoś szuka szybkiej wersji:

1) Utwórz niestandardową klasę dla swoich danych

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) Aby zapisać dane, użyj następującej funkcji:

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

3) Aby pobrać:

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

Uwaga: tutaj zapisuję i pobieram tablicę obiektów klasy niestandardowej.

Ankit Goel
źródło
9

Biorąc odpowiedź @ chrissr i działając z nią, ten kod można zaimplementować w ładnej kategorii, NSUserDefaultsaby zapisywać i pobierać niestandardowe obiekty:

@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

Stosowanie:

[[NSUserDefaults standardUserDefaults] saveCustomObject:myObject key:@"myKey"];
Carlos P.
źródło
7

Szybki 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")
    }
    }

do przechowywania i pobierania:

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
        }
Wiaczesław
źródło
Przypomnienie: selfnie jest obowiązkowe przed zmiennymi w initi encode.
Tamás Sengel
Czy możesz pokazać, w jaki sposób inicjujesz obiekt za pomocą NSCodera
Abhishek Thapliyal
@AbhishekThapliyal, nie rozumiem cię. init (koder aDecoder: NSCoder) inicjuje go
Wiaczesław
6

Zsynchronizuj dane / obiekt, które zapisałeś w NSUserDefaults

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

Mam nadzieję, że to ci pomoże. Dzięki

Muhammad Aamir Ali
źródło
Zwróć uwagę na osoby z przyszłości, które od Swift 3 w ogóle nie powinny wywoływać synchronizacji.
Jose Ramirez
@JozemiteApps dlaczego? czy możesz zamieścić link z wyjaśnieniem tego tematu?
code4latte
@ code4latte Nie wiem, czy to zostało zmienione, ale dokumentacja Apple stwierdza, że ​​„Metoda synchronize (), która jest automatycznie wywoływana w okresowych odstępach czasu, utrzymuje pamięć podręczną w pamięci w synchronizacji z domyślną bazą danych użytkownika”. Wcześniej było napisane, że powinieneś to nazywać, JEŚLI masz pewność, że potrzebujesz natychmiast zapisanych danych. Czytałem już kilka razy, że użytkownik nie powinien już tego nazywać.
Jose Ramirez