Як зберігати власні об’єкти в NSUserDefaults


268

Гаразд, значить, я робив щось, що колючись, і я усвідомлюю свою проблему, але не знаю, як її виправити. Я створив спеціальний клас для зберігання деяких даних. Я роблю об’єкти для цього класу, і мені потрібно, щоб вони тривали між сесіями. До того, як я вносив всю свою інформацію NSUserDefaults, але це не працює.

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

Це повідомлення про помилку, яке я отримую, коли я кладу свій власний клас "Player" в NSUserDefaults. Тепер я прочитав, що, мабуть, NSUserDefaultsзберігається лише деякі види інформації. То як я можу потрапляти до своїх об'єктів NSUSerDefaults?

Я читав, що повинен бути спосіб "кодувати" мій користувацький об'єкт, а потім вкласти його, але я не впевнений, як його реалізувати, допомога буде оцінена! Дякую!

**** EDIT ****

Добре, тому я працював з наведеним нижче кодом (Дякую!), Але у мене все ще є проблеми. В основному код зараз виходить з ладу, і я не впевнений, чому, оскільки він не дає помилок. Можливо, мені не вистачає чогось базового і я просто занадто втомився, але ми побачимо. Ось реалізація мого спеціального класу "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

Файл реалізації:

#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

Отже, це мій клас, досить прямо вперед, я знаю, що він працює у створенні моїх предметів. Отже, ось відповідні частини файлу AppDelegate (де я викликаю функції шифрування та дешифрування):

@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

А потім важливі частини файлу реалізації:

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

Ееее, вибачте за весь код. Просто намагаюся допомогти. В основному, додаток запуститься, а потім вийде з ладу негайно. Я звузив його до частини програми для шифрування, саме там він виходить з ладу, тому я щось роблю не так, але не знаю, що. Допомога буде вдячна ще раз, дякую!

(Я ще не збирався розшифровувати, оскільки я ще не почав шифрувати роботу.)


Чи є у вас стежка стека або більше інформації про збій, наприклад, номер рядка, що спричиняє збій? Я не одразу бачу нічого поганого в коді, тому корисна відправна точка.
chrissr

У наведеному вище прикладі ви використовували encodeObject для зберігання self.life, який є int. Натомість слід використовувати encodeInt.
брут

Відповіді:


516

У вашому класі програвача реалізуйте наступні два способи (заміни викликів на encodeObject чимось відповідним вашому власному об'єкту):

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

Читання та письмо з 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;
}

Код безсоромно запозичений у: збереження класу в nsuserdefaults


Відредагував мою публікацію вище, щоб відобразити мої зміни.
Етан Мік

@chrissr у вас помилка в налаштуваннях за замовчуванням NSUserDefaults = [NSUserDefaults standardUserDefaults]; ... має бути NSUserDefaults * за замовчуванням.
Меггі

2
NSKeyedArchiver скелі ... здається, він навіть автоматично опускається в об'єкти NSArray або NSDictionary і кодує будь-які користувацькі об'єкти, що кодуються, всередині.
BadPirate

2
Я не буду педантичним, просто справжнім питанням, чи не проти яблучних вказівок використовувати синтезовані сеттери, тобто self.property в методах init?
Самхан Салахуддін

2
@chrissr Змініть NSData *myEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];на NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];. Змінні не збігаються.
GoRoS

36

Я створюю бібліотеку RMMapper ( https://github.com/roomorama/RMMapper ), щоб допомогти зберегти користувацький об’єкт у NSUserDefaults простіше і зручніше, адже реалізація encodeWithCoder та initWithCoder - це супер нудно!

Щоб позначити клас як архівований, просто використовуйте: #import "NSObject+RMArchivable.h"

Щоб зберегти спеціальний об'єкт у NSUserDefaults:

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

Щоб отримати спеціальний obj з NSUserDefaults:

user = [defaults rm_customObjectForKey:@"SAVED_DATA"]; 

Ваша бібліотека передбачає, що у вас є властивості для всього, що ви хочете зберегти, і що ви хочете зберегти все, для чого є властивості. Якщо це правда, то ваша бібліотека, безумовно, може бути корисною, але я думаю, ви виявите, що в багатьох випадках це не так.
Ерік Б

Корисно зберігати лише звичайний об’єкт, я б сказав, його використання досить схоже на Gson у Java. Які справи ви дивитесь?
1313 року

7
Це було дуже корисно, радий, що я читав відповіді; D
Альбара

що робити, коли мій користувацький об’єкт має у власність інший користувацький об’єкт?
Шиаль

@Shial: якщо зробити інший спеціальний об’єкт архіваційним, він також буде збережений
thomasdao

35

Швидкий 4

Представлено протокол Codable , який робить усі чари для подібних завдань. Просто підпорядковуйте свою власну структуру / клас до цього:

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

А для зберігання у налаштуваннях за замовчуванням ви можете використовувати PropertyListEncoder / декодер :

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)

Так буде працювати і для масивів та інших класів контейнерів таких об'єктів:

try! PropertyListDecoder().decode([Player].self, from: storedArray)

1
Працює як шарм
Натан Баррето

Зауважте, що ви отримуєте Codableповедінку безкоштовно лише тоді, коли всі члени екземплярів самі є, Codableінакше вам доведеться самостійно написати код кодування,
BallpointBen

1
А точніше, просто відповідайте цим типам членів Codable. І так далі, рекурсивно. Тільки у дуже спеціалізованих випадках вам потрібно буде написати код кодування.
Сергіу Тодіраску

Найпростіший спосіб з Swift 4.
alstr

31

Якщо хтось шукає швидку версію:

1) Створіть спеціальний клас для своїх даних

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) Для збереження даних використовуйте наступну функцію:

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

3) Щоб отримати:

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

Примітка. Тут я зберігаю і витягую масив об'єктів спеціального класу.


9

Отримавши відповідь @ chrissr і працюючи з нею, цей код можна перетворити на приємну категорію NSUserDefaultsдля збереження та отримання користувацьких об’єктів:

@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

Використання:

[[NSUserDefaults standardUserDefaults] saveCustomObject:myObject key:@"myKey"];

7

Швидкий 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")
    }
    }

зберігати та завантажувати:

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
        }

Просто нагадування: selfне є обов'язковим перед змінними в initі encode.
Тамас Сенгель

Чи можете ви показати, як ви інтимулюєте об’єкт із NSCoder
Абхішек Тапліял

@AbhishekThapliyal, я тебе не розумію. init (coder aDecoder: NSCoder) ініціалізує його
В'ячеслав

6

Синхронізуйте дані / об'єкт, які ви зберегли в NSUserDefaults

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

Сподіваюся, що це вам допоможе. Дякую


Зауважте людей з майбутнього, які, як і Swift 3, "синхронізувати", взагалі не повинні викликати вас.
Хосе Рамірес

@JozemiteApps чому? чи ви можете опублікувати посилання з поясненнями на цю тему?
code4latte

@ code4latte Я не знаю, чи було це змінено, але в документації Apple зазначено, що "метод синхронізації (), який автоматично викликається з періодичними інтервалами, зберігає кеш пам'яті в синхронізації з базою даних за замовчуванням користувача". Раніше він говорив, що вам слід називати його лише в тому випадку, якщо ви впевнені, що вам потрібні дані, що зберігаються миттєво. Кілька разів я читав, що користувач більше не повинен його телефонувати.
Хосе Рамірес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.