217

I thought I knew what was causing this error, but I can't seem to figure out what I did wrong.

Here is the full error message I am getting:

Attempt to set a non-property-list object (
   "<BC_Person: 0x8f3c140>"
) as an NSUserDefaults value for key personDataArray

I have a Person class that I think is conforming to the NSCoding protocol, where I have both of these methods in my person class:

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

At some point in the app, the NSString in the BC_PersonClass is set, and I have a DataSave class that I think is handling the encoding the properties in my BC_PersonClass. Here is the code I am using from the DataSave class:

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

I hope this is enough code to give you an idea o what I am trying to do. Again I know my problem lie with how I am encoding my properties in my BC_Person class, I just can't seem to figure out what though I'm doing wrong.

Thanks for the help!

rmaddy
  • 314,917
  • 42
  • 532
  • 579
icekomo
  • 9,328
  • 7
  • 31
  • 59

12 Answers12

284

The code you posted tries to save an array of custom objects to NSUserDefaults. You can't do that. Implementing the NSCoding methods doesn't help. You can only store things like NSArray, NSDictionary, NSString, NSData, NSNumber, and NSDate in NSUserDefaults.

You need to convert the object to NSData (like you have in some of the code) and store that NSData in NSUserDefaults. You can even store an NSArray of NSData if you need to.

When you read back the array you need to unarchive the NSData to get back your BC_Person objects.

Perhaps you want this:

- (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"];
}
jtbandes
  • 115,675
  • 35
  • 233
  • 266
rmaddy
  • 314,917
  • 42
  • 532
  • 579
  • One question I have. If I wanted to add the personEncodedObject into an array and then put the array into user Data... could i just replace: [archiveArray addObject:personEncodedObject]; with a NSArray and add the addObject:personEncodedObject into that array and then save that in userData? If you follow what I'm saying. – icekomo Nov 01 '13 at 04:35
  • Huh? I think you make a typo since you want to replace a line of code with the same line of code. My code does put the array of encoded objects in the user defaults. – rmaddy Nov 01 '13 at 04:37
  • I guess I'm lost as I thought you could only use NSArray with userDefaults, but I see in your example you're using NSMutable array. Maybe I'm just not understanding something... – icekomo Nov 01 '13 at 04:40
  • 2
    `NSMutableArray` extends `NSArray`. It's perfectly fine to pass an `NSMutableArray` to any method that expects an `NSArray`. Keep in mind that the array actually stored in `NSUserDefaults` will be immutable when you read it back. – rmaddy Nov 01 '13 at 04:42
  • Ok, that makes sense, so then all I should have to do is set the immutable array to a mutable array so that I add more data to the array. Thank you for your answers, this was very helpful! – icekomo Nov 01 '13 at 04:44
  • Be a little careful using mutable arrays. If you do something like [nsud setObject:myMutableArray forKey:@"mutableArray"];, then a few minutes later do [myMutableArray addObject:@"something"]; [nsud setObject:myMutableArray forKey:@"mutableArray"];, NSUserDefaults will go "oh! That's the same object, I already have that. No need to set it again". – Catfish_Man Nov 01 '13 at 04:54
  • @Catfish_Man Works fine with `NSMutableArray`. – derpoliuk Dec 11 '13 at 19:09
  • NSMutableArray in general works fine. The *specific scenario* I described is not reliable. – Catfish_Man Dec 11 '13 at 19:54
  • I would use NSDictionary with my custom objects instead of nsdata – Muhammad Hewedy Sep 15 '14 at 22:48
  • @rmaddy: This is great and correct, but what's the full list for "things like NSArray, NSDictionary..."? Thank you! – Dan Rosenstark Aug 20 '15 at 19:18
  • Hey your save code totally work but how can i load array again ? because list convert to byte – Gökhan Çokkeçeci Sep 03 '15 at 11:44
  • When I used ur code I faced this issue Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[FKChannel encodeWithCoder:]: unrecognized selector sent to instance 0x7fb97a4bfc50' – Ahd Radwan Oct 06 '15 at 12:00
  • 1
    Does this cost anything in performance? for example if this was running through a loop and populating a custom class object many times then changing it to NSData before adding each one to an array, would this have any greater performance issue than just passing normal data types to the array? – Rob85 Dec 17 '15 at 21:16
67

It seems rather wasteful to me to run through the array and encode the objects into NSData yourself. Your error BC_Person is a non-property-list object is telling you that the framework doesn't know how to serialize your person object.

So all that is needed is to ensure that your person object conforms to NSCoding then you can simply convert your array of custom objects into NSData and store that to defaults. Heres a playground:

Edit: Writing to NSUserDefaults is broken on Xcode 7 so the playground will archive to data and back and print an output. The UserDefaults step is included in case its fixed at a later point

//: 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")
}

And Voila, you have stored an array of custom objects into NSUserDefaults

Daniel Galasko
  • 23,617
  • 8
  • 77
  • 97
  • This answer is solid! I had my objects conforming with NSCoder, but I forgot about the array they where stored in. Thanks, saved me many hours! – Mikael Hellman Nov 22 '15 at 21:18
  • 1
    @Daniel, I just pasted your code as is into playground xcode 7.3 and it is giving an error on "let retrievedPeople: [Person] = retrievePeople()!" -> EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0). What adjustments do I need to make? Thanks – rockhammer Jun 29 '16 at 13:27
  • 1
    @rockhammer looks like NSUserDefaults doesn't work in Playgrounds since Xcode 7:( will update shortly – Daniel Galasko Jun 29 '16 at 15:54
  • 1
    @Daniel, no problem. I went ahead and inserted your code into my project and it works like a charm! My object class has an NSDate in addition to 3 String types. I just needed to substitute NSDate for String in the decode func. Thanks – rockhammer Jun 29 '16 at 18:33
  • For Swift3: ```func encode(with aCoder: NSCoder)``` – Achintya Ashok Feb 06 '17 at 05:54
  • @DanielGalasko Can you please also put Delete Functionality from User Defaults and give example?? – Parth Barot May 08 '19 at 07:32
  • Hi @ParthBarot that is not really the original question unless I misinterpreted. There is a very clear API for removing objects if you check the documentation. Take a look here https://developer.apple.com/documentation/foundation/userdefaults If it is still unclear, ask a new question :) – Daniel Galasko May 09 '19 at 15:16
52

To save:

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

To Get:

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

For Remove

[currentDefaults removeObjectForKey:@"yourKeyName"];
neowinston
  • 7,584
  • 10
  • 52
  • 83
jose920405
  • 7,982
  • 6
  • 45
  • 71
35

Swift 3 Solution

Simple utility class

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

}

Model Class

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

    }
}

How to call

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
  • 37,929
  • 33
  • 189
  • 256
14

Swift- 4 Xcode 9.1

try this code

you can not store mapper in NSUserDefault, you can only store NSData, NSString, NSNumber, NSDate, NSArray, or 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
  • 4,618
  • 3
  • 29
  • 40
  • 12
    Thank-you for this. NSKeyedArchiver.archivedData(withRootObject:) has been deprecated in iOS12 to a new method that throws an error. Perhaps an update to your code? :) – Marcy Sep 15 '18 at 21:02
13

Swift with @propertyWrapper

Save Codable object to UserDefault

@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
}

Example User model confirm Codable

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

How to use it

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

//GetValue
print(GlobalSettings.user)
dimohamdy
  • 2,917
  • 30
  • 28
11

First off, rmaddy's answer (above) is right: implementing NSCoding doesn't help. However, you need to implement NSCoding to use NSKeyedArchiver and all that, so it's just one more step... converting via NSData.

Example methods

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

So you can wrap your NSCoding objects in an NSArray or NSDictionary or whatever...

Dan Rosenstark
  • 68,471
  • 58
  • 283
  • 421
8

I had this problem trying save a dictionary to NSUserDefaults. It turns out it wouldn't save because it contained NSNull values. So I just copied the dictionary into a mutable dictionary removed the nulls then saved to NSUserDefaults

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

In this case I knew which keys might be NSNull values.

simple_code
  • 707
  • 9
  • 24
6

Swift 5: The Codable protocol can be used instead of NSKeyedArchiever.

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

The Pref struct is custom wrapper around the UserDefaults standard object.

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

So you can use it this way:

Pref.user?.name = "John"

if let user = Pref.user {...
Krešimir Prcela
  • 4,257
  • 33
  • 46
  • 1
    `if let data = UserDefaults.standard.data(forKey: keyUser) {` and Btw casting from `User` to `User` is pointless just `return try JSONDecoder().decode(User.self, from: data)` – Leo Dabus Jul 14 '19 at 01:36
5

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

A default object must be a property list—that is, an instance of (or for collections, a combination of instances of): NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary.

If you want to store any other type of object, you should typically archive it to create an instance of NSData. For more details, see Preferences and Settings Programming Guide.

Community
  • 1
  • 1
4

Swift 5 Very Easy way

//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
  • 5,361
  • 1
  • 43
  • 34
1

I ran into this and eventually figured out it was because I was trying to use NSNumber as dictionary keys, and property lists only allow strings as keys. The documentation for setObject:forKey: doesn't mention this limitation, but the About Property Lists page that it links to does:

By convention, each Cocoa and Core Foundation object listed in Table 2-1 is called a property-list object. Conceptually, you can think of “property list” as being an abstract superclass of all these classes. If you receive a property list object from some method or function, you know that it must be an instance of one of these types, but a priori you may not know which type. If a property-list object is a container (that is, an array or dictionary), all objects contained within it must also be property-list objects. If an array or dictionary contains objects that are not property-list objects, then you cannot save and restore the hierarchy of data using the various property-list methods and functions. And although NSDictionary and CFDictionary objects allow their keys to be objects of any type, if the keys are not string objects, the collections are not property-list objects.

(Emphasis mine)

Tom Hamming
  • 10,577
  • 11
  • 71
  • 145