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

May 2024 ยท 5 minute read

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: "http://www.example-job.com") 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: "http://www.example-job.com") 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)

ncG1vNJzZmirpJawrLvVnqmfpJ%2Bse6S7zGiorp2jqbawutJoa2prZWqBc4OOmqutnZ2lwW7Azmagp6uVp8Fuus6nZKmqn6Wys8DYZqOiq6RivKO2xJyrZq%2BYmrtuwNGyoKefXam8br%2FAr5xmmV2YwrTAzqZkqJqamrC1ecg%3D