Tentative de définir un objet sans liste de propriétés en tant que NSUserDefaults

196

Je pensais savoir ce qui causait cette erreur, mais je n'arrive pas à comprendre ce que j'ai fait de mal.

Voici le message d'erreur complet que je reçois:

Essayez de définir un objet ne figurant pas dans la liste des propriétés (
   "<BC_Person: 0x8f3c140>"
) en tant que valeur NSUserDefaults pour la clé personDataArray

J'ai une Personclasse qui je pense est conforme au NSCodingprotocole, où j'ai ces deux méthodes dans ma classe personne:

- (void)encodeWithCoder:(NSCoder *)coder {
    [coder encodeObject:self.personsName forKey:@"BCPersonsName"];
    [coder encodeObject:self.personsBills forKey:@"BCPersonsBillsArray"];
}

- (id)initWithCoder:(NSCoder *)coder {
    self = [super init];
    if (self) {
        self.personsName = [coder decodeObjectForKey:@"BCPersonsName"];
        self.personsBills = [coder decodeObjectForKey:@"BCPersonsBillsArray"];
    }
    return self;
}

À un certain moment dans l'application, le NSStringdans BC_PersonClassest défini, et j'ai une DataSaveclasse qui, je pense, gère l'encodage des propriétés dans mon BC_PersonClass. Voici le code que j'utilise dans la DataSaveclasse:

- (void)savePersonArrayData:(BC_Person *)personObject
{
   // NSLog(@"name of the person %@", personObject.personsName);

    [mutableDataArray addObject:personObject];

    // set the temp array to the mutableData array
    tempMuteArray = [NSMutableArray arrayWithArray:mutableDataArray];

    // save the person object as nsData
    NSData *personEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:personObject];

    // first add the person object to the mutable array
    [tempMuteArray addObject:personEncodedObject];

    // NSLog(@"Objects in the array %lu", (unsigned long)mutableDataArray.count);

    // now we set that data array to the mutable array for saving
    dataArray = [[NSArray alloc] initWithArray:mutableDataArray];
    //dataArray = [NSArray arrayWithArray:mutableDataArray];

    // save the object to NS User Defaults
    NSUserDefaults *userData = [NSUserDefaults standardUserDefaults];
    [userData setObject:dataArray forKey:@"personDataArray"];
    [userData synchronize];
}

J'espère que c'est assez de code pour vous donner une idée de ce que j'essaie de faire. Encore une fois, je sais que mon problème réside dans la façon dont j'encode mes propriétés dans ma classe BC_Person, je n'arrive pas à comprendre ce que je fais mal.

Merci pour l'aide!

icekomo
la source
Je me
that I think is conforming to the NSCoding protocolil est super facile d'ajouter des tests unitaires pour cela, et ça vaut vraiment le coup.
rr1g0
Le mieux est de vérifier vos paramètres.J'ai découvert que j'ajoutais une chaîne qui était un nombre.Ainsi, cela ne doit pas être utilisé, d'où le problème.
Rajal

Réponses:

272

Le code que vous avez publié tente d'enregistrer un tableau d'objets personnalisés dans NSUserDefaults. Tu ne peux pas faire ça. L'implémentation des NSCodingméthodes n'aide pas. Vous ne pouvez stocker des choses comme NSArray, NSDictionary, NSString, NSData, NSNumberet NSDatedans NSUserDefaults.

Vous devez convertir l'objet NSData(comme vous l'avez fait dans certains codes) et le stocker NSDatadans NSUserDefaults. Vous pouvez même stocker un NSArrayde NSDatasi vous avez besoin.

Lorsque vous relisez le tableau, vous devez désarchiver le NSDatapour récupérer vos BC_Personobjets.

Peut-être que vous voulez ceci:

- (void)savePersonArrayData:(BC_Person *)personObject {
    [mutableDataArray addObject:personObject];

    NSMutableArray *archiveArray = [NSMutableArray arrayWithCapacity:mutableDataArray.count];
    for (BC_Person *personObject in mutableDataArray) { 
        NSData *personEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:personObject];
        [archiveArray addObject:personEncodedObject];
    }

    NSUserDefaults *userData = [NSUserDefaults standardUserDefaults];
    [userData setObject:archiveArray forKey:@"personDataArray"];
}
rmaddy
la source
J'ai une question. Si je voulais ajouter le personEncodedObject dans un tableau, puis mettre le tableau dans les données utilisateur ... pourrais-je simplement remplacer: [archiveArray addObject: personEncodedObject]; avec un NSArray et ajoutez l'addObject: personEncodedObject dans ce tableau, puis enregistrez-le dans userData? Si vous suivez ce que je dis.
icekomo
Hein? Je pense que vous faites une faute de frappe car vous voulez remplacer une ligne de code par la même ligne de code. Mon code place le tableau d'objets encodés dans les valeurs par défaut de l'utilisateur.
rmaddy
Je suppose que je suis perdu car je pensais que vous ne pouviez utiliser NSArray qu'avec userDefaults, mais je vois dans votre exemple que vous utilisez un tableau NSMutable. Peut-être que je ne comprends tout simplement pas quelque chose ...
icekomo
2
NSMutableArrays'étend NSArray. C'est parfaitement bien de passer un NSMutableArrayà n'importe quelle méthode qui attend un NSArray. Gardez à l'esprit que le tableau réellement stocké dans NSUserDefaultssera immuable lorsque vous le relirez.
rmaddy
1
Cela coûte-t-il quelque chose en termes de performances? par exemple, si cela s'exécutait dans une boucle et remplissait un objet de classe personnalisé plusieurs fois, puis en le changeant en NSData avant d'ajouter chacun à un tableau, cela aurait-il un problème de performances plus important que de simplement passer des types de données normaux au tableau?
Rob85
66

Il me semble plutôt inutile de parcourir le tableau et de coder vous-même les objets dans NSData. Votre erreur BC_Person is a non-property-list objectvous indique que le framework ne sait pas comment sérialiser votre objet personne.

Donc, tout ce qui est nécessaire est de vous assurer que votre objet personne est conforme au NSCoding, vous pouvez simplement convertir votre tableau d'objets personnalisés en NSData et le stocker en valeurs par défaut. Voici un terrain de jeu:

Modifier : L'écriture dans NSUserDefaultsest interrompue sur Xcode 7, de sorte que le terrain de jeu archivera les données et sauvegardera et imprimera une sortie. L'étape UserDefaults est incluse au cas où elle serait corrigée ultérieurement

//: Playground - noun: a place where people can play

import Foundation

class Person: NSObject, NSCoding {
    let surname: String
    let firstname: String

    required init(firstname:String, surname:String) {
        self.firstname = firstname
        self.surname = surname
        super.init()
    }

    //MARK: - NSCoding -
    required init(coder aDecoder: NSCoder) {
        surname = aDecoder.decodeObjectForKey("surname") as! String
        firstname = aDecoder.decodeObjectForKey("firstname") as! String
    }

    func encodeWithCoder(aCoder: NSCoder) {
        aCoder.encodeObject(firstname, forKey: "firstname")
        aCoder.encodeObject(surname, forKey: "surname")
    }
}

//: ### Now lets define a function to convert our array to NSData

func archivePeople(people:[Person]) -> NSData {
    let archivedObject = NSKeyedArchiver.archivedDataWithRootObject(people as NSArray)
    return archivedObject
}

//: ### Create some people

let people = [Person(firstname: "johnny", surname:"appleseed"),Person(firstname: "peter", surname: "mill")]

//: ### Archive our people to NSData

let peopleData = archivePeople(people)

if let unarchivedPeople = NSKeyedUnarchiver.unarchiveObjectWithData(peopleData) as? [Person] {
    for person in unarchivedPeople {
        print("\(person.firstname), you have been unarchived")
    }
} else {
    print("Failed to unarchive people")
}

//: ### Lets try use NSUserDefaults
let UserDefaultsPeopleKey = "peoplekey"
func savePeople(people:[Person]) {
    let archivedObject = archivePeople(people)
    let defaults = NSUserDefaults.standardUserDefaults()
    defaults.setObject(archivedObject, forKey: UserDefaultsPeopleKey)
    defaults.synchronize()
}

func retrievePeople() -> [Person]? {
    if let unarchivedObject = NSUserDefaults.standardUserDefaults().objectForKey(UserDefaultsPeopleKey) as? NSData {
        return NSKeyedUnarchiver.unarchiveObjectWithData(unarchivedObject) as? [Person]
    }
    return nil
}

if let retrievedPeople = retrievePeople() {
    for person in retrievedPeople {
        print("\(person.firstname), you have been unarchived")
    }
} else {
    print("Writing to UserDefaults is still broken in playgrounds")
}

Et voilà, vous avez stocké un tableau d'objets personnalisés dans NSUserDefaults

Daniel Galasko
la source
Cette réponse est solide! J'avais mes objets conformes à NSCoder, mais j'ai oublié le tableau dans lequel ils étaient stockés. Merci, cela m'a fait gagner de nombreuses heures!
Mikael Hellman
1
@Daniel, je viens de coller votre code tel quel dans la cour de récréation xcode 7.3 et cela donne une erreur sur "let retrievedPeople: [Person] = retrievePeople ()!" -> EXC_BAD_INSTRUCTION (code = EXC_I386_INVOP, sous-code = 0x0). Quels ajustements dois-je faire? Merci
rockhammer
1
@rockhammer semble que NSUserDefaults ne fonctionne pas dans Playgrounds depuis Xcode 7 :( sera mis à jour sous peu
Daniel Galasko
1
@Daniel, pas de problème. Je suis allé de l'avant et j'ai inséré votre code dans mon projet et cela fonctionne comme un charme! Ma classe d'objets a une NSDate en plus de 3 types de chaînes. Je devais juste remplacer NSDate par String dans le func de décodage. Merci
rockhammer
Pour Swift3:func encode(with aCoder: NSCoder)
Achintya Ashok
51

Sauver:

NSUserDefaults *currentDefaults = [NSUserDefaults standardUserDefaults];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:yourObject];
[currentDefaults setObject:data forKey:@"yourKeyName"];

Obtenir:

NSData *data = [currentDefaults objectForKey:@"yourKeyName"];
yourObjectType * token = [NSKeyedUnarchiver unarchiveObjectWithData:data];

Pour supprimer

[currentDefaults removeObjectForKey:@"yourKeyName"];
jose920405
la source
34

Solution Swift 3

Classe utilitaire simple

class ArchiveUtil {

    private static let PeopleKey = "PeopleKey"

    private static func archivePeople(people : [Human]) -> NSData {

        return NSKeyedArchiver.archivedData(withRootObject: people as NSArray) as NSData
    }

    static func loadPeople() -> [Human]? {

        if let unarchivedObject = UserDefaults.standard.object(forKey: PeopleKey) as? Data {

            return NSKeyedUnarchiver.unarchiveObject(with: unarchivedObject as Data) as? [Human]
        }

        return nil
    }

    static func savePeople(people : [Human]?) {

        let archivedObject = archivePeople(people: people!)
        UserDefaults.standard.set(archivedObject, forKey: PeopleKey)
        UserDefaults.standard.synchronize()
    }

}

Classe de modèle

class Human: NSObject, NSCoding {

    var name:String?
    var age:Int?

    required init(n:String, a:Int) {

        name = n
        age = a
    }


    required init(coder aDecoder: NSCoder) {

        name = aDecoder.decodeObject(forKey: "name") as? String
        age = aDecoder.decodeInteger(forKey: "age")
    }


    public func encode(with aCoder: NSCoder) {

        aCoder.encode(name, forKey: "name")
        aCoder.encode(age, forKey: "age")

    }
}

Comment appeler

var people = [Human]()

people.append(Human(n: "Sazzad", a: 21))
people.append(Human(n: "Hissain", a: 22))
people.append(Human(n: "Khan", a: 23))

ArchiveUtil.savePeople(people: people)

let others = ArchiveUtil.loadPeople()

for human in others! {

    print("name = \(human.name!), age = \(human.age!)")
}
Sazzad Hissain Khan
la source
1
Agréable! : D la meilleure adaptation rapide +1
Gabo Cuadros Cardenas
developer.apple.com/documentation/foundation/userdefaults/… ... "cette méthode est inutile et ne doit pas être utilisée." LOL ... quelqu'un chez apple n'est pas un joueur d'équipe.
Nerdy Bunz
12

Swift- 4 Xcode 9.1

essayez ce code

vous ne pouvez pas stocker le mappeur dans NSUserDefault, vous pouvez uniquement stocker NSData, NSString, NSNumber, NSDate, NSArray ou NSDictionary.

let myData = NSKeyedArchiver.archivedData(withRootObject: myJson)
UserDefaults.standard.set(myData, forKey: "userJson")

let recovedUserJsonData = UserDefaults.standard.object(forKey: "userJson")
let recovedUserJson = NSKeyedUnarchiver.unarchiveObject(with: recovedUserJsonData)
Vishal Vaghasiya
la source
8
Merci pour ça. NSKeyedArchiver.archivedData (withRootObject :) a été déconseillé dans iOS12 à une nouvelle méthode qui génère une erreur. Peut-être une mise à jour de votre code? :)
Marcy
10

Tout d'abord, la réponse de rmaddy (ci-dessus) est correcte: l'implémentation NSCodingn'aide pas. Cependant, vous devez implémenter NSCodingpour utiliser NSKeyedArchiveret tout cela, donc ce n'est qu'une étape de plus ... la conversion via NSData.

Exemples de méthodes

- (NSUserDefaults *) defaults {
    return [NSUserDefaults standardUserDefaults];
}

- (void) persistObj:(id)value forKey:(NSString *)key {
    [self.defaults setObject:value  forKey:key];
    [self.defaults synchronize];
}

- (void) persistObjAsData:(id)encodableObject forKey:(NSString *)key {
    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:encodableObject];
    [self persistObj:data forKey:key];
}    

- (id) objectFromDataWithKey:(NSString*)key {
    NSData *data = [self.defaults objectForKey:key];
    return [NSKeyedUnarchiver unarchiveObjectWithData:data];
}

Vous pouvez donc envelopper vos NSCodingobjets dans un NSArrayou NSDictionaryou autre ...

Dan Rosenstark
la source
6

J'ai eu ce problème en essayant d'enregistrer un dictionnaire dans NSUserDefaults. Il s'avère qu'il ne serait pas enregistré car il contenait des NSNullvaleurs. Je viens donc de copier le dictionnaire dans un dictionnaire mutable supprimé les null puis enregistré dansNSUserDefaults

NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithDictionary:dictionary_trying_to_save];
[dictionary removeObjectForKey:@"NullKey"];
[[NSUserDefaults standardUserDefaults] setObject:dictionary forKey:@"key"];

Dans ce cas, je savais quelles clés pouvaient être des NSNullvaleurs.

simple_code
la source
4

Swift 5: le protocole codable peut être utilisé à la place de NSKeyedArchiever .

struct User: Codable {
    let id: String
    let mail: String
    let fullName: String
}

La structure Pref est un wrapper personnalisé autour de l'objet standard UserDefaults.

struct Pref {
    static let keyUser = "Pref.User"
    static var user: User? {
        get {
            if let data = UserDefaults.standard.object(forKey: keyUser) as? Data {
                do {
                    return try JSONDecoder().decode(User.self, from: data)
                } catch {
                    print("Error while decoding user data")
                }
            }
            return nil
        }
        set {
            if let newValue = newValue {
                do {
                    let data = try JSONEncoder().encode(newValue)
                    UserDefaults.standard.set(data, forKey: keyUser)
                } catch {
                    print("Error while encoding user data")
                }
            } else {
                UserDefaults.standard.removeObject(forKey: keyUser)
            }
        }
    }
}

Vous pouvez donc l'utiliser de cette façon:

Pref.user?.name = "John"

if let user = Pref.user {...
Prcela
la source
1
if let data = UserDefaults.standard.data(forKey: keyUser) {et Btw casting de Userto Userest inutilereturn try JSONDecoder().decode(User.self, from: data)
Leo Dabus
4

Rapide avec@propertyWrapper

Enregistrer l' Codableobjet dansUserDefault

@propertyWrapper
    struct UserDefault<T: Codable> {
        let key: String
        let defaultValue: T

        init(_ key: String, defaultValue: T) {
            self.key = key
            self.defaultValue = defaultValue
        }

        var wrappedValue: T {
            get {

                if let data = UserDefaults.standard.object(forKey: key) as? Data,
                    let user = try? JSONDecoder().decode(T.self, from: data) {
                    return user

                }

                return  defaultValue
            }
            set {
                if let encoded = try? JSONEncoder().encode(newValue) {
                    UserDefaults.standard.set(encoded, forKey: key)
                }
            }
        }
    }




enum GlobalSettings {

    @UserDefault("user", defaultValue: User(name:"",pass:"")) static var user: User
}

Exemple de modèle d'utilisateur confirmable codable

struct User:Codable {
    let name:String
    let pass:String
}

Comment l'utiliser

//Set value 
 GlobalSettings.user = User(name: "Ahmed", pass: "Ahmed")

//GetValue
print(GlobalSettings.user)
dimo hamdy
la source
C'est la meilleure réponse moderne. Stacker, utilisez cette réponse est vraiment évolutive.
Stefan Vasiljevic
3

https://developer.apple.com/reference/foundation/userdefaults

Un objet par défaut doit être une liste de propriétés, c'est-à-dire une instance de (ou pour les collections, une combinaison d'instances de): NSData, NSString, NSNumber, NSDate, NSArray ou NSDictionary.

Si vous souhaitez stocker tout autre type d'objet, vous devez généralement l'archiver pour créer une instance de NSData. Pour plus de détails, consultez le Guide de programmation des préférences et paramètres.

Isaak Osipovich Dunayevsky
la source
3

Swift 5 Très facile

//MARK:- First you need to encoded your arr or what ever object you want to save in UserDefaults
//in my case i want to save Picture (NMutableArray) in the User Defaults in
//in this array some objects are UIImage & Strings

//first i have to encoded the NMutableArray 
let encodedData = NSKeyedArchiver.archivedData(withRootObject: yourArrayName)
//MARK:- Array save in UserDefaults
defaults.set(encodedData, forKey: "YourKeyName")

//MARK:- When you want to retreive data from UserDefaults
let decoded  = defaults.object(forKey: "YourKeyName") as! Data
yourArrayName = NSKeyedUnarchiver.unarchiveObject(with: decoded) as! NSMutableArray

//MARK: Enjoy this arrry "yourArrayName"
Shakeel Ahmed
la source
c'est seulement pour la structure codable
Kishore Kumar
J'obtiens: 'archivedData (withRootObject :)' a été déconseillé dans macOS 10.14: Use + archivedDataWithRootObject: requireSecureCoding: error: instead
multitudes