Cara menyimpan objek khusus di NSUserDefaults

268

Baiklah, jadi saya telah melakukan beberapa pencarian, dan saya menyadari masalah saya, tetapi saya tidak tahu bagaimana cara memperbaikinya. Saya telah membuat kelas khusus untuk menampung beberapa data. Saya membuat objek untuk kelas ini, dan saya perlu mereka untuk bertahan di antara sesi. Sebelum saya memasukkan semua informasi saya NSUserDefaults, tetapi ini tidak berfungsi.

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

Itu adalah pesan kesalahan yang saya dapatkan ketika saya menempatkan kelas kustom saya, "Player", di menu NSUserDefaults. Sekarang, saya sudah membaca yang ternyata NSUserDefaultshanya menyimpan beberapa jenis informasi. Jadi bagaimana saya mendapatkan objek saya NSUSerDefaults?

Saya membaca bahwa harus ada cara untuk "menyandikan" objek kustom saya dan kemudian memasukkannya, tetapi saya tidak yakin bagaimana cara mengimplementasikannya, bantuan akan dihargai! Terima kasih!

**** EDIT ****

Baiklah, jadi saya bekerja dengan kode yang diberikan di bawah ini (Terima kasih!), Tapi saya masih mengalami beberapa masalah. Pada dasarnya, kode mogok sekarang dan saya tidak yakin mengapa, karena tidak memberikan kesalahan. Mungkin aku melewatkan sesuatu yang mendasar dan aku terlalu lelah, tapi kita akan lihat. Ini adalah implementasi kelas Custom saya, "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

File Implementasi:

#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

Jadi itu kelas saya, cukup lurus ke depan, saya tahu itu berfungsi dalam membuat objek saya. Jadi di sini adalah bagian yang relevan dari file AppDelegate (tempat saya memanggil fungsi enkripsi dan dekripsi):

@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

Dan kemudian bagian penting dari file implementasi:

    #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, maaf tentang semua kode. Hanya berusaha membantu. Pada dasarnya, aplikasi akan diluncurkan dan kemudian crash dengan segera. Saya mempersempitnya ke bagian enkripsi aplikasi, di situlah crash, jadi saya melakukan sesuatu yang salah tetapi saya tidak yakin apa. Bantuan akan dihargai kembali, terima kasih!

(Saya belum sempat mendekripsi, karena saya belum mengenkripsi bekerja.)

Ethan Mick
sumber
Apakah Anda memiliki jejak tumpukan atau informasi lebih lanjut tentang kerusakan, seperti nomor baris mana yang menyebabkan kerusakan? Saya tidak segera melihat ada yang salah dengan kode, jadi titik awal akan sangat membantu.
chrissr
Dalam contoh di atas Anda telah menggunakan encodeObject untuk menyimpan self.life yang merupakan int. Anda harus menggunakan encodeInt sebagai gantinya.
broot

Jawaban:

516

Pada kelas Player Anda, terapkan dua metode berikut (mengganti panggilan ke encodeObject dengan sesuatu yang relevan dengan objek Anda sendiri):

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

Membaca dan menulis dari 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;
}

Kode dipinjam tanpa malu dari: saving class in nsuserdefaults

Chrissr
sumber
Mengedit posting saya di atas untuk mencerminkan perubahan saya.
Ethan Mick
@chrissr Anda memiliki kesalahan di NSUserDefaults defaults = [NSUserDefaults standardUserDefaults]; ... harus NSUserDefaults * default.
Maggie
2
NSKeyedArchiver rocks ... tampaknya bahkan secara otomatis turun ke objek NSArray atau NSDictionary dan mengkodekan objek kustom yang dapat disandikan di dalamnya.
BadPirate
2
Saya tidak bertele-tele, hanya pertanyaan yang asli, bukankah itu bertentangan dengan pedoman apel untuk menggunakan setter buatan yaitu self.property dalam metode init?
Samhan Salahuddin
2
@ chrissr Silakan ubah NSData *myEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];untuk NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];. Variabel tidak cocok.
GoRoS
36

Saya membuat perpustakaan RMMapper ( https://github.com/roomorama/RMMapper ) untuk membantu menyimpan objek khusus ke NSUserDefaults lebih mudah dan nyaman, karena menerapkan encodeWithCoder dan initWithCoder sangat membosankan!

Untuk menandai kelas sebagai arsip, cukup gunakan: #import "NSObject+RMArchivable.h"

Untuk menyimpan objek khusus ke NSUserDefaults:

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

Untuk mendapatkan objek khusus dari NSUserDefaults:

user = [defaults rm_customObjectForKey:@"SAVED_DATA"]; 
thomasdao
sumber
Pustaka Anda berasumsi bahwa Anda memiliki properti untuk semua yang ingin Anda pertahankan dan bahwa Anda ingin mempertahankan semua properti yang ingin Anda miliki. Jika ini benar, perpustakaan Anda tentu bisa membantu, tetapi saya pikir Anda akan menemukan bahwa dalam banyak kasus tidak.
Erik B
Sangat berguna untuk mempertahankan objek biasa saja, saya akan mengatakan penggunaannya sangat mirip dengan Gson di Jawa. Kasus apa yang Anda lihat?
thomasdao
7
Itu sangat membantu, senang saya terus membaca jawaban, D
Albara
bagaimana dengan ketika objek kustom saya miliki di properti objek kustom lainnya?
Shial
@Shial: jika Anda membuat objek khusus lainnya dapat diarsipkan, itu akan disimpan juga
thomasdao
35

Cepat 4

Memperkenalkan protokol Codable yang melakukan semua keajaiban untuk tugas-tugas semacam ini. Cukup sesuaikan struct / kelas khusus Anda dengan itu:

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

Dan untuk menyimpan di Default Anda dapat menggunakan 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)

Ini akan berfungsi seperti itu untuk array dan kelas-kelas kontainer lain dari objek-objek seperti itu juga:

try! PropertyListDecoder().decode([Player].self, from: storedArray)
Sergiu Todirascu
sumber
1
Bekerja seperti pesona
Nathan Barreto
Perhatikan bahwa Anda hanya mendapatkan Codableperilaku secara gratis ketika semua anggota contoh adalah diri mereka sendiri Codable- jika tidak, Anda harus menulis sendiri beberapa kode penyandian,
BallpointBen
1
Atau lebih tepatnya menyesuaikan tipe-tipe anggota itu Codablejuga. Dan seterusnya, secara rekursif. Hanya dalam kasus yang sangat khusus Anda perlu menulis kode penyandian.
Sergiu Todirascu
Cara termudah dengan Swift 4.
alstr
31

Jika ada yang mencari versi cepat:

1) Buat kelas khusus untuk data Anda

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) Untuk menyimpan data, gunakan fungsi berikut:

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

3) Untuk mengambil:

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

Catatan: Di sini saya menyimpan dan mengambil larik objek kelas kustom.

Ankit Goel
sumber
9

Mengambil jawaban @ chrissr dan menjalankannya, kode ini dapat diimplementasikan ke dalam kategori yang bagus NSUserDefaultsuntuk menyimpan dan mengambil objek khusus:

@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

Pemakaian:

[[NSUserDefaults standardUserDefaults] saveCustomObject:myObject key:@"myKey"];
Carlos P
sumber
7

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

untuk menyimpan dan mengambil:

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
sumber
Hanya pengingat: selftidak wajib sebelum variabel dalam initdan encode.
Tamás Sengel
Bisakah Anda menunjukkan bagaimana Anda menginternalisasi objek dengan NSCoder
Abhishek Thapliyal
@AbhishekThapliyal, saya tidak mengerti Anda. init (coder aDecoder: NSCoder) menginisialisasi
Vyacheslav
6

Sinkronisasi data / objek yang telah Anda simpan ke dalam NSUserDefaults

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

Semoga ini bisa membantu Anda. Terima kasih

Muhammad Aamir Ali
sumber
Perhatikan orang-orang dari masa depan bahwa pada Swift 3, "sinkronisasi" tidak boleh dipanggil sama sekali oleh Anda.
Jose Ramirez
@JozemiteApps mengapa? atau dapatkah Anda memposting tautan dengan penjelasan untuk topik ini?
code4latte
@ code4latte Saya tidak tahu apakah itu diubah tetapi dokumentasi Apple menyatakan bahwa "Metode sinkronisasi (), yang secara otomatis dipanggil pada interval berkala, menjaga cache dalam memori tetap sinkron dengan database default pengguna." Sebelumnya, dikatakan bahwa Anda hanya boleh menyebutnya JIKA Anda yakin Anda membutuhkan data yang disimpan secara instan. Saya telah membaca sekitar beberapa kali bahwa pengguna tidak boleh memanggilnya lagi.
Jose Ramirez