Attempt to insert non-property list object when trying to save a custom object in Swift 3

I have a simple object which conforms to the NSCoding protocol.

import Foundation class JobCategory: NSObject, NSCoding { var id: Int var name: String var URLString: String init(id: Int, name: String, URLString: String) { self.id = id self.name = name self.URLString = URLString } // MARK: - NSCoding required init(coder aDecoder: NSCoder) { id = aDecoder.decodeObject(forKey: "id") as? Int ?? aDecoder.decodeInteger(forKey: "id") name = aDecoder.decodeObject(forKey: "name") as! String URLString = aDecoder.decodeObject(forKey: "URLString") as! String } func encode(with aCoder: NSCoder) { aCoder.encode(id, forKey: "id") aCoder.encode(name, forKey: "name") aCoder.encode(URLString, forKey: "URLString") } } 

I'm trying to save an instance of it in UserDefaults but it keeps failing with the following error.

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object for key jobCategory'

This is the code where I'm saving in UserDefaults.

enum UserDefaultsKeys: String { case jobCategory } class ViewController: UIViewController { @IBAction func didTapSaveButton(_ sender: UIButton) { let category = JobCategory(id: 1, name: "Test Category", URLString: "") let userDefaults = UserDefaults.standard userDefaults.set(category, forKey: UserDefaultsKeys.jobCategory.rawValue) userDefaults.synchronize() } } 

I replaced the enum value to key with a normal string but the same error still occurs. Any idea what's causing this?

2

6 Answers

You need to create Data instance from your JobCategory model using JSONEncoder and store that Data instance in UserDefaults and later decode using JSONDecoder.

struct JobCategory: Codable { let id: Int let name: String } // To store in UserDefaults if let encoded = try? JSONEncoder().encode(category) { UserDefaults.standard.set(encoded, forKey: UserDefaultsKeys.jobCategory.rawValue) } // Retrieve from UserDefaults if let data = UserDefaults.standard.object(forKey: UserDefaultsKeys.jobCategory.rawValue) as? Data, let category = try? JSONDecoder().decode(JobCategory.self, from: data) { print(category.name) } 

Old Answer

You need to create Data instance from your JobCategory instance using archivedData(withRootObject:) and store that Data instance in UserDefaults and later unarchive using unarchiveTopLevelObjectWithData(_:), So try like this.

For Storing data in UserDefaults

let category = JobCategory(id: 1, name: "Test Category", URLString: "") let encodedData = NSKeyedArchiver.archivedData(withRootObject: category, requiringSecureCoding: false) let userDefaults = UserDefaults.standard userDefaults.set(encodedData, forKey: UserDefaultsKeys.jobCategory.rawValue) 

For retrieving data from UserDefaults

let decoded = UserDefaults.standard.object(forKey: UserDefaultsKeys.jobCategory.rawValue) as! Data let decodedTeams = NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(decoded) as! JobCategory print(decodedTeams.name) 
7

Update Swift 4, Xcode 10

I have written a struct around it for easy access.

//set, get & remove User own profile in cache struct UserProfileCache { static let key = "userProfileCache" static func save(_ value: Profile!) { UserDefaults.standard.set(try? PropertyListEncoder().encode(value), forKey: key) } static func get() -> Profile! { var userData: Profile! if let data = UserDefaults.standard.value(forKey: key) as? Data { userData = try? PropertyListDecoder().decode(Profile.self, from: data) return userData! } else { return userData } } static func remove() { UserDefaults.standard.removeObject(forKey: key) } } 

Profile is a Json encoded object.

struct Profile: Codable { let id: Int! let firstName: String let dob: String! } 

Usage:

//save details in user defaults... UserProfileCache.save(profileDetails) 

Hope that helps!!!

Thanks

1

Swift save Codable object to UserDefault with @propertyWrapper

@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) 
2

Save dictionary Into userdefault

let data = NSKeyedArchiver.archivedData(withRootObject: DictionaryData) UserDefaults.standard.set(data, forKey: kUserData) 

Retrieving the dictionary

let outData = UserDefaults.standard.data(forKey: kUserData) let dict = NSKeyedUnarchiver.unarchiveObject(with: outData!) as! NSDictionary 
1

Based on Harjot Singh answer. I've used like this:

struct AppData { static var myObject: MyObject? { get { if UserDefaults.standard.object(forKey: "UserLocationKey") != nil { if let data = UserDefaults.standard.value(forKey: "UserLocationKey") as? Data { let myObject = try? PropertyListDecoder().decode(MyObject.self, from: data) return myObject! } } return nil } set { UserDefaults.standard.set(try? PropertyListEncoder().encode(newValue), forKey: "UserLocationKey") } } } 

Here's a UserDefaults extension to set and get a Codable object, and keep it human-readable in the plist (User Defaults) if you open it as a plain text file:

extension Encodable { var asDictionary: [String: Any]? { guard let data = try? JSONEncoder().encode(self) else { return nil } return try? JSONSerialization.jsonObject(with: data) as? [String : Any] } } extension Decodable { init?(dictionary: [String: Any]) { guard let data = try? JSONSerialization.data(withJSONObject: dictionary) else { return nil } guard let object = try? JSONDecoder().decode(Self.self, from: data) else { return nil } self = object } } extension UserDefaults { func setEncodableAsDictionary<T: Encodable>(_ encodable: T, for key: String) { self.set(encodable.asDictionary, forKey: key) } func getDecodableFromDictionary<T: Decodable>(for key: String) -> T? { guard let dictionary = self.dictionary(forKey: key) else { return nil } return T(dictionary: dictionary) } } 

If you want to also support array (of codables) to and from plist array, add the following to the extension:

extension UserDefaults { func setEncodablesAsArrayOfDictionaries<T: Encodable>(_ encodables: Array<T>, for key: String) { let arrayOfDictionaries = encodables.map({ $0.asDictionary }) self.set(arrayOfDictionaries, forKey: key) } func getDecodablesFromArrayOfDictionaries<T: Decodable>(for key: String) -> [T]? { guard let arrayOfDictionaries = self.array(forKey: key) as? [[String: Any]] else { return nil } return arrayOfDictionaries.compactMap({ T(dictionary: $0) }) } } 

If you don't care about plist being human-readable, it can be simply saved as Data (will look like random string if opened as plain text):

extension UserDefaults { func setEncodable<T: Encodable>(_ encodable: T, for key: String) throws { let data = try PropertyListEncoder().encode(encodable) self.set(data, forKey: key) } func getDecodable<T: Decodable>(for key: String) -> T? { guard self.object(forKey: key) != nil, let data = self.value(forKey: key) as? Data else { return nil } let obj = try? PropertyListDecoder().decode(T.self, from: data) return obj } } 

(With this second approach, you don't need the Encodable and Decodable extensions from the top)

Your Answer

Sign up or log in

Sign up using Google Sign up using Facebook Sign up using Email and Password

Post as a guest

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

You Might Also Like