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?
26 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)
7Update 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
1Swift 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)
2Save 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
1Based 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