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!
that I think is conforming to the NSCoding protocol
it is super easy to add unit testing for that, and really worth it.
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"];
}
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
func encode(with aCoder: NSCoder)
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"];
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!)")
}
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)
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...
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)
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.
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.
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 {...
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)
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"
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)
Success story sharing
NSMutableArray
extendsNSArray
. It's perfectly fine to pass anNSMutableArray
to any method that expects anNSArray
. Keep in mind that the array actually stored inNSUserDefaults
will be immutable when you read it back.