วิธีเก็บวัตถุที่กำหนดเองใน NSUserDefaults


268

เอาล่ะฉันได้ทำอะไรบ้างแล้วฉันก็ตระหนักถึงปัญหาของฉัน แต่ฉันไม่รู้วิธีแก้ไข ฉันสร้างคลาสที่กำหนดเองเพื่อเก็บข้อมูลบางอย่าง ฉันทำวัตถุสำหรับชั้นนี้และฉันต้องการให้พวกเขาสุดท้ายระหว่างช่วง ก่อนที่ฉันจะใส่ข้อมูลทั้งหมดของฉันเข้าไปNSUserDefaultsแต่สิ่งนี้ไม่ทำงาน

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

นั่นคือข้อผิดพลาดที่ฉันได้รับเมื่อฉันใส่ชั้นเรียนของฉันที่กำหนดเอง "ผู้เล่น" NSUserDefaultsใน ตอนนี้ฉันได้อ่านที่เห็นได้ชัดว่าNSUserDefaultsเก็บข้อมูลบางประเภทเท่านั้น แล้วฉันจะนำวัตถุของฉันเข้าไปได้NSUSerDefaultsอย่างไร

ฉันอ่านว่าควรมีวิธีการ "เข้ารหัส" วัตถุที่กำหนดเองของฉันแล้ววางไว้ แต่ฉันไม่แน่ใจว่าจะใช้งานได้อย่างไรความช่วยเหลือจะได้รับการชื่นชม! ขอบคุณ!

**** **** แก้ไข

เอาล่ะฉันทำงานกับรหัสที่ระบุด้านล่าง (ขอบคุณ!) แต่ฉันยังคงมีปัญหาอยู่ โดยพื้นฐานแล้วรหัสขัดข้องตอนนี้และฉันไม่แน่ใจว่าทำไมเพราะมันไม่ได้มีข้อผิดพลาดใด ๆ บางทีฉันขาดพื้นฐานบางอย่างและฉันเหนื่อยเกินไป แต่เราจะเห็น นี่คือการใช้งานคลาสที่กำหนดเองของฉัน "ผู้เล่น":

@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 แทน
broot

คำตอบ:


516

ในคลาส Player ของคุณใช้สองวิธีต่อไปนี้ (การแทนที่การโทรเพื่อเข้ารหัส Object ด้วยสิ่งที่เกี่ยวข้องกับวัตถุของคุณเอง):

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


แก้ไขโพสต์ของฉันด้านบนเพื่อสะท้อนการเปลี่ยนแปลงของฉัน
Ethan Mick

@chrissr คุณมีข้อผิดพลาดใน NSUserDefaults defaults = [NSUserDefaults standardUserDefaults]; ... ควรเป็นค่าเริ่มต้น NSUserDefaults *
Maggie

2
ก้อนหิน NSKeyedArchiver ... ดูเหมือนว่ามันจะเข้าสู่วัตถุ NSArray หรือ NSDictionary โดยอัตโนมัติและเข้ารหัสวัตถุที่กำหนดเองที่เข้ารหัสได้ภายใน
BadPirate

2
ฉันไม่ได้เป็นคนเชื่องช้า แต่เป็นคำถามที่แท้จริงไม่ใช่กับแนวทางของแอปเปิ้ลที่จะใช้ setters สังเคราะห์เช่น self.property ในวิธีการเริ่มต้น?
Samhan Salahuddin

2
@chrissr กรุณาเปลี่ยนสำหรับNSData *myEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:object]; NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];ตัวแปรไม่ตรงกัน
GoRoS

36

ฉันสร้างไลบรารี่ RMMapper ( https://github.com/roomorama/RMMapper ) เพื่อช่วยบันทึกออบเจกต์ที่กำหนดเองลงใน NSUserDefaults ง่ายขึ้นและสะดวกมากขึ้นเพราะการใช้ encodeWithCoder และ initWithCoder น่าเบื่อมาก!

ในการทำเครื่องหมายคลาสที่เป็น archivable ให้ใช้: #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"]; 

ไลบรารีของคุณสมมติว่าคุณมีคุณสมบัติสำหรับทุกสิ่งที่คุณต้องการยืนยันและคุณต้องการยืนยันทุกสิ่งที่คุณมีอยู่ หากเป็นจริงห้องสมุดของคุณอาจมีประโยชน์ แต่ฉันคิดว่าคุณจะพบว่าในหลาย ๆ กรณีไม่เป็นเช่นนั้น
Erik B

มันมีประโยชน์ที่จะคงอยู่วัตถุธรรมดาเท่านั้นฉันจะบอกว่าการใช้งานมันค่อนข้างคล้ายกับ Gson ใน Java กรณีใดบ้างที่คุณมอง
thomasdao

7
นั่นเป็นประโยชน์จริง ๆ ฉันดีใจที่ได้อ่านคำตอบต่อไป D
Albara

สิ่งที่เกี่ยวกับเมื่อวัตถุที่กำหนดเองของฉันมีในคุณสมบัติวัตถุที่กำหนดเองอื่น ๆ
Shial

@Shial: ถ้าคุณสร้างวัตถุที่กำหนดเองอื่น ๆ ที่เก็บได้มันจะถูกบันทึกไว้ด้วย
thomasdao

35

สวิฟต์ 4

แนะนำโปรโตคอลCodableที่ใช้เวทมนตร์สำหรับงานประเภทนี้ เพียงแค่ปรับโครงสร้าง / คลาสที่คุณกำหนดเอง:

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

และสำหรับการจัดเก็บในค่าเริ่มต้นคุณสามารถใช้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)

มันจะทำงานเช่นนั้นสำหรับอาร์เรย์และคลาสคอนเทนเนอร์อื่น ๆ ของวัตถุเช่นกัน:

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

1
ใช้งานได้อย่างมีเสน่ห์
Nathan Barreto

โปรดทราบว่าคุณจะได้รับCodableพฤติกรรมฟรีเมื่อสมาชิกอินสแตนซ์ทั้งหมดเป็นของตัวเองCodable- ไม่เช่นนั้นคุณต้องเขียนโค้ดการเข้ารหัสด้วยตัวคุณเอง
BallpointBen

1
หรือค่อนข้างสอดคล้องกับประเภทสมาชิกเหล่านั้นCodableด้วย และอื่น ๆ ซ้ำ เฉพาะในกรณีที่กำหนดเองมากคุณจะต้องเขียนรหัสการเข้ารหัส
Sergiu Todirascu

วิธีที่ง่ายที่สุดกับสวิฟท์ 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
Tamás Sengel

คุณสามารถแสดงให้เห็นว่าคุณกำลังทำให้วัตถุน่าสนใจยิ่งขึ้นด้วย NSCoder อย่างไร
Abhishek Thapliyal

@AbhishekThapliyal ฉันไม่เข้าใจคุณ init (coder aDecoder: NSCoder) เริ่มต้นมัน
Vyacheslav

6

ซิงโครไนซ์ข้อมูล / วัตถุที่คุณบันทึกไว้ใน NSUserDefaults

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

หวังว่านี่จะช่วยคุณได้ ขอบคุณ


สังเกตผู้คนจากอนาคตว่าในขณะที่ Swift 3 คุณไม่ควรเรียก "ซิงโครไนซ์" เลย
Jose Ramirez

@JozemiteApps ทำไมล่ะ หรือคุณสามารถโพสต์ลิงค์พร้อมคำอธิบายสำหรับหัวข้อนี้?
code4latte

@ code4latte ฉันไม่ทราบว่ามีการเปลี่ยนแปลงหรือไม่ แต่เอกสารของ Apple ระบุว่า "วิธีการซิงโครไนซ์ () ซึ่งเรียกใช้โดยอัตโนมัติตามช่วงเวลาเป็นระยะทำให้แคชในหน่วยความจำซิงค์กับฐานข้อมูลเริ่มต้นของผู้ใช้" ก่อนหน้านี้มันบอกว่าคุณควรจะเรียกมันว่าถ้าคุณแน่ใจว่าคุณต้องการข้อมูลที่บันทึกไว้ทันที ฉันได้อ่านประมาณสองสามครั้งว่าผู้ใช้ไม่ควรเรียกอีกต่อไป
Jose Ramirez
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.