Get 'NSInvalidArgumentException' 'Attempt to insert non-property list object when saving class type array to UserDefaults [duplicate] - swift

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?

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)

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

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)

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

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)

Related

Add multiple Structs to one UserDefaults entity

I have my UserDefaults like this
fileprivate enum userdefaultKeys: String {
userSearchHistory = "userSearchHistory",
}
extension UserDefaults {
static func getUserSearchHistory() -> SearchHistory? {
let data = self.standard.data(forKey: userdefaultKeys.userSearchHistory.rawValue)
return SearchHistory.decode(json: data)
}
static func setUserSearchHistory(userSearchHistory: SearchHistory?) {
guard let json: Any = userSearchHistory?.json else { return }
self.standard.set(json, forKey: userdefaultKeys.userSearchHistory.rawValue)
}
}
And I'm saving this data to the UserDefaults
struct SearchHistory: Codable {
let type: SearchHistoryEnum
let name: String
let corpNo: String
let storeNo: String
let long: Double
let lat: Double
}
enum SearchHistoryEnum: Codable {
case storeSearch
case jsonSearch
}
let historySearch = SearchHistory(type: SearchHistoryEnum.storeSearch, name: store?.storename ?? "", corpNo: store?.corpno ?? "", storeNo: store?.storeno ?? "", long: longtitude, lat: latitude)
UserDefaults.setUserSearchHistory(userSearchHistory: historySearch)
This is okay, but it saves only one instance of SearchHistory in the time. I would like to have max 5. When 6th instance comes, I would like to delete the most old one
First of all your enum doesn't compile, you probably mean
fileprivate enum UserdefaultKeys: String {
case userSearchHistory
}
It's not necessary to specify the raw value.
Second of all I highly recommend not to fight the framework and to conform to the UserDefaults pattern to overload the getter and setter. I don't know your special methods for decoding and encoding, this is a simple implementation with standard JSONDecoder and JSONEncoder
extension UserDefaults {
func searchHistory(forKey key: String = UserdefaultKeys.userSearchHistory.rawValue) -> SearchHistory? {
guard let data = data(forKey: key) else { return nil }
return try? JSONDecoder().decode(SearchHistory.self, from: data)
}
func searchHistory(forKey key: String = UserdefaultKeys.userSearchHistory.rawValue) -> [SearchHistory]? {
guard let data = data(forKey: key) else { return nil }
return try? JSONDecoder().decode([SearchHistory].self, from: data)
}
func set(_ value: SearchHistory, forKey key: String = UserdefaultKeys.userSearchHistory.rawValue) {
guard let data = try? JSONEncoder().encode(value) else { return }
set(data, forKey: key)
}
func set(_ value: [SearchHistory], forKey key: String = UserdefaultKeys.userSearchHistory.rawValue) {
guard let data = try? JSONEncoder().encode(value) else { return }
set(data, forKey: key)
}
}
The benefit is the syntax is always the same for a single item or for an array
let historySearch = SearchHistory(type: SearchHistoryEnum.storeSearch, name: store?.storename ?? "", corpNo: store?.corpno ?? "", storeNo: store?.storeno ?? "", long: longtitude, lat: latitude)
UserDefaults.standard.set(historySearch)
or
UserDefaults.standard.set([historySearch])
The code to delete the oldest put in the method to write the data.

Creating a expiry date for UserDefaults values

So I had this idea of creating an expiry for UserDefaults. This is the approach I was starting to take but I'm stuck.
struct TimedObject<T: Codable>: Codable {
let object: T
let expireDate: Date
}
and then:
extension UserDefaults {
func set<T: Codable>(_ value: T, forKey key: String, expireDate: Date) {
let timedObject = TimedObject<T>(object: value, expireDate: expireDate)
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(timedObject) {
UserDefaults.standard.set(encoded, forKey: key)
}
override open class func value(forKey key: String) -> Any? {
guard let value = self.value(forKey: key) else {
return nil
}
if TimedObject<???>.self == type(of: value) { // This is where I'm stuck
...
}
}
So if I would name the type and not use generics I would easily solve this. But naturally I prefer to use generics. Can this be done?
I know OP is using a struct to wrap the stored value but I would still like to offer a different protocol based solution where any type that should be stored with an expiration date needs to conform to this protocol.
Here is the protocol for I am using
protocol TimedObject: Codable {
associatedtype Value: Codable
var value: Value { get }
var expirationDate: Date { get }
}
and the functions to store and retrieve from UserDefaults
extension UserDefaults {
func set<Timed: TimedObject>(_ value: Timed, forKey key: String) {
if let encoded = try? JSONEncoder().encode(value) {
self.set(encoded, forKey: key)
}
}
func value<Timed: TimedObject>(_ type: Timed.Type, forKey key: String) -> Timed.Value? {
guard let data = self.value(forKey: key) as? Data, let object = try? JSONDecoder().decode(Timed.self, from: data) else {
return nil
}
return object.expirationDate > .now ? object.value : nil
}
}
Finally an example
struct MyStruct: Codable {
let id: Int
let name: String
}
extension MyStruct: TimedObject {
typealias Value = Self
var value: MyStruct { self }
var expirationDate: Date {
.now.addingTimeInterval(24 * 60 * 60)
}
}
let my = MyStruct(id: 12, name: "abc")
UserDefaults.standard.set(my, forKey: "my")
let my2 = UserDefaults.standard.value(MyStruct.self, forKey: "my")
Since you're returning Any?, it is best to create another struct to point to TimedObject as you don't need the object property when returning Any?:
struct Expiry: Codable {
var expireDate: Date
}
struct TimedObject<T: Codable>: Timable {
let object: T
var expireDate: Date
}
override open class func value(forKey key: String) -> Any? {
guard let value = self.value(forKey: key) as? Data else {
return nil
}
if let timed = try? JSONDecoder().decode(Expiry.self, from: value) {
//do anything with timed
}
//make sure to return value
}
And here's a method to access TimedObject:
func timedObject<T: Codable>(forKey key: String) -> TimedObject<T>? {
guard let value = self.data(forKey: key) as? Data, let timed = try? JSONDecoder().decode(TimedObject<T>.self, from: value) else {
return nil
}
return value
}
If you make the value() method generic then you can reverse the process done in the set() method: retrieve the data and decode it as a TimedObject<T>.
However, I would choose a different name to avoid possible ambiguities with the exisiting value(forKey:) method. Also I see no reason why this should be a class method.
Note also that your generic set() method should call the non-generic version on the same instance.
extension UserDefaults {
func set<T: Codable>(_ value: T, forKey key: String, expireDate: Date) {
let timedObject = TimedObject(object: value, expireDate: expireDate)
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(timedObject) {
set(encoded, forKey: key)
}
}
func expiringValue<T: Codable>(forKey key: String) -> T? {
guard let data = self.data(forKey: key) else {
return nil
}
let decoder = JSONDecoder()
guard let decoded = try? decoder.decode(TimedObject<T>.self, from: data) else {
return nil
}
// check expire date ...
return decoded.object
}
}
Example usage:
let val1 = UserDefaults.standard.expiringValue(forKey: "foo") as String?
let val2: String? = UserDefaults.standard.expiringValue(forKey: "bar")
In both cases, expiringValue(forKey:) is called with the inferred type.
Or in combination with optional binding:
if let val: String = UserDefaults.standard.expiringValue(forKey: "test") {
print(val)
}
Another option is to pass the desired type as an additional argument:
func value<T: Codable>(forKey key: String, as: T.Type) -> T? {
guard let data = self.data(forKey: key) else {
return nil
}
let decoder = JSONDecoder()
guard let decoded = try? decoder.decode(TimedObject<T>.self, from: data) else {
return nil
}
// check expire date ...
return decoded.object
}
which is then used as
let val = UserDefaults.standard.value(forKey: "foo", as: String.self)

How to store and retrieve data in userdefault? (Ex:Array of Struct )

struct ExpandableNames:Codable {
var isExpanded: Bool
var names: [String]
var icons: [String]
}
var names = ["Food/Drink", "Tours", "Transport", "Gifts","Flights", "Shopping", "Activities","Entertainment","Accomodation","Other"]
var icons = ["Food_Category", "Tours", "Transport_Category", "Gifts_Category","Flights_Category","Shopping_Category","Activities_category","Entertainment_category", "Accomodation_Category","Other_Category"]
var categoryWholeArray = [Int:ExpandableNames]()
How to store categoryWholeArray to user default?
I tried
Store userDefault:
UserDefaults.standard.set(try? PropertyListEncoder().encode(categoryWholeArray), forKey:"categoryWholeArray")
Retrieve data problem is here
if let data = UserDefaults.standard.value(forKey:"categoryWholeArray") as? Data {
let songs2 = try? PropertyListDecoder().decode(Array<categoryWholeArray.values>.self, from: data)
}
Anyone tried?
You need to encode data while storing in UserDefaults and when you try to get those data you must need to decode it.
Here is extension i have that might help you.
extension UserDefaults {
func setCustomObjToUserDefaults(CustomeObj: AnyObject, forKey:String) {
let defaults = UserDefaults.standard
let encodedData = NSKeyedArchiver.archivedData(withRootObject: CustomeObj)
defaults.set(encodedData, forKey: forKey)
defaults.synchronize()
}
func getCustomObjFromUserDefaults(forKey:String) -> AnyObject? {
let defaults = UserDefaults.standard
if defaults.object(forKey: forKey) != nil {
if let decoded = defaults.object(forKey: forKey) as? Data {
let decodedTeams = NSKeyedUnarchiver.unarchiveObject(with:decoded) as AnyObject
return decodedTeams
}
}
return nil
}
func setJsonObject<T: Encodable>(encodable: T, forKey key: String) {
if let data = try? JSONEncoder().encode(encodable) {
set(data, forKey: key)
}
}
func getJsonObject<T: Decodable>(_ type: T.Type, forKey key: String) -> T? {
if let data = object(forKey: key) as? Data,
let value = try? JSONDecoder().decode(type, from: data) {
return value
}
return nil
}
func removeCustomObject(forKey:String)
{
let defaults = UserDefaults.standard
defaults.removeObject(forKey: forKey)
defaults.synchronize()
}
}
All correct except
if let data = UserDefaults.standard.value(forKey:"categoryWholeArray") as? Data {
let songs2 = try? PropertyListDecoder().decode([Int:ExpandableNames].self, from: data)
print(songs2)
}

NSKeyedArchiver: casting Data returning nil - Swift

Well, investigated several similar topics here, done everything as suggested, but my computed property "previousUserData" returns me nil, when trying to cast the decoded object to my type. What's wrong?
#objc class PreviousUserData: NSObject, NSCoding {
var name: String
var phone: String
var email: String
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
aCoder.encode(phone, forKey: "phone")
aCoder.encode(email, forKey: "email")
}
required convenience init?(coder aDecoder: NSCoder) {
guard
let name = aDecoder.decodeObject(forKey: "name") as? String,
let phone = aDecoder.decodeObject(forKey: "phone") as? String,
let email = aDecoder.decodeObject(forKey: "email") as? String
else {
return nil
}
self.init(name: name, phone: phone, email: email)
}
init(name: String, phone: String, email: String) {
self.name = name
self.phone = phone
self.email = email
}
}
unarchived returns me nil, but data for key "userdata" is exists
var previousUserData: PreviousUserData? {
get {
if let object = UserDefaults.standard.object(forKey: "userdata") as? Data {
let unarchived = NSKeyedUnarchiver.unarchiveObject(with: object) as? PreviousUserData
return unarchived
}
return nil
}
set {
let encodedData: Data = NSKeyedArchiver.archivedData(withRootObject: previousUserData as Any)
UserDefaults.standard.setValue(encodedData, forKey: "userdata")
}
}
Actually you can't get valid data because the setter is wrong. You have to save newValue rather than previousUserData.
This is an slightly optimized version
var previousUserData: PreviousUserData? {
get {
guard let data = UserDefaults.standard.data(forKey: "userdata") else { return nil }
return NSKeyedUnarchiver.unarchiveObject(with: data) as? PreviousUserData
}
set {
guard let newValue = newValue else { return }
let encodedData = NSKeyedArchiver.archivedData(withRootObject: newValue)
UserDefaults.standard.set(encodedData, forKey: "userdata")
}
}
NSCoding is pretty heavy. In this case I'd recommend to use Codable to serialize the data as JSON or Property List. It gets rid of #objc, class and NSObject and reduces the entire code to
struct PreviousUserData : Codable {
var name: String
var phone: String
var email: String
}
var previousUserData: PreviousUserData? {
get {
guard let data = UserDefaults.standard.data(forKey: "userdata") else { return nil }
return try? JSONDecoder().decode(PreviousUserData.self, from: data)
}
set {
guard let newValue = newValue, let encodedData = try? JSONEncoder().encode(newValue) else { return }
UserDefaults.standard.set(encodedData, forKey: "userdata")
}
}

How can I use Swift’s Codable to encode into a dictionary?

I have a struct that implements Swift 4’s Codable. Is there a simple built-in way to encode that struct into a dictionary?
let struct = Foo(a: 1, b: 2)
let dict = something(struct)
// now dict is ["a": 1, "b": 2]
If you don't mind a bit of shifting of data around you could use something like this:
extension Encodable {
func asDictionary() throws -> [String: Any] {
let data = try JSONEncoder().encode(self)
guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
throw NSError()
}
return dictionary
}
}
Or an optional variant
extension Encodable {
var dictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
}
}
Assuming Foo conforms to Codable or really Encodable then you can do this.
let struct = Foo(a: 1, b: 2)
let dict = try struct.asDictionary()
let optionalDict = struct.dictionary
If you want to go the other way(init(any)), take a look at this Init an object conforming to Codable with a dictionary/array
Here are simple implementations of DictionaryEncoder / DictionaryDecoder that wrap JSONEncoder, JSONDecoder and JSONSerialization, that also handle encoding / decoding strategies…
class DictionaryEncoder {
private let encoder = JSONEncoder()
var dateEncodingStrategy: JSONEncoder.DateEncodingStrategy {
set { encoder.dateEncodingStrategy = newValue }
get { return encoder.dateEncodingStrategy }
}
var dataEncodingStrategy: JSONEncoder.DataEncodingStrategy {
set { encoder.dataEncodingStrategy = newValue }
get { return encoder.dataEncodingStrategy }
}
var nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy {
set { encoder.nonConformingFloatEncodingStrategy = newValue }
get { return encoder.nonConformingFloatEncodingStrategy }
}
var keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy {
set { encoder.keyEncodingStrategy = newValue }
get { return encoder.keyEncodingStrategy }
}
func encode<T>(_ value: T) throws -> [String: Any] where T : Encodable {
let data = try encoder.encode(value)
return try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any]
}
}
class DictionaryDecoder {
private let decoder = JSONDecoder()
var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy {
set { decoder.dateDecodingStrategy = newValue }
get { return decoder.dateDecodingStrategy }
}
var dataDecodingStrategy: JSONDecoder.DataDecodingStrategy {
set { decoder.dataDecodingStrategy = newValue }
get { return decoder.dataDecodingStrategy }
}
var nonConformingFloatDecodingStrategy: JSONDecoder.NonConformingFloatDecodingStrategy {
set { decoder.nonConformingFloatDecodingStrategy = newValue }
get { return decoder.nonConformingFloatDecodingStrategy }
}
var keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy {
set { decoder.keyDecodingStrategy = newValue }
get { return decoder.keyDecodingStrategy }
}
func decode<T>(_ type: T.Type, from dictionary: [String: Any]) throws -> T where T : Decodable {
let data = try JSONSerialization.data(withJSONObject: dictionary, options: [])
return try decoder.decode(type, from: data)
}
}
Usage is similar to JSONEncoder / JSONDecoder…
let dictionary = try DictionaryEncoder().encode(object)
and
let object = try DictionaryDecoder().decode(Object.self, from: dictionary)
For convenience, I've put this all in a repo… https://github.com/ashleymills/SwiftDictionaryCoding
I have create a library called CodableFirebase and it's initial purpose was to use it with Firebase Database, but it does actually what you need: it creates a dictionary or any other type just like in JSONDecoder but you don't need to do the double conversion here like you do in other answers. So it would look something like:
import CodableFirebase
let model = Foo(a: 1, b: 2)
let dict = try! FirebaseEncoder().encode(model)
There is no built in way to do that.
As answered above if you have no performance issues then you can accept the JSONEncoder + JSONSerialization implementation.
But I would rather go the standard library's way to provide an encoder/decoder object.
class DictionaryEncoder {
private let jsonEncoder = JSONEncoder()
/// Encodes given Encodable value into an array or dictionary
func encode<T>(_ value: T) throws -> Any where T: Encodable {
let jsonData = try jsonEncoder.encode(value)
return try JSONSerialization.jsonObject(with: jsonData, options: .allowFragments)
}
}
class DictionaryDecoder {
private let jsonDecoder = JSONDecoder()
/// Decodes given Decodable type from given array or dictionary
func decode<T>(_ type: T.Type, from json: Any) throws -> T where T: Decodable {
let jsonData = try JSONSerialization.data(withJSONObject: json, options: [])
return try jsonDecoder.decode(type, from: jsonData)
}
}
You can try it with following code:
struct Computer: Codable {
var owner: String?
var cpuCores: Int
var ram: Double
}
let computer = Computer(owner: "5keeve", cpuCores: 8, ram: 4)
let dictionary = try! DictionaryEncoder().encode(computer)
let decodedComputer = try! DictionaryDecoder().decode(Computer.self, from: dictionary)
I am force-trying here to make the example shorter. In production code you should handle the errors appropriately.
I'm not sure if it's the best way but you definitely can do something like:
struct Foo: Codable {
var a: Int
var b: Int
init(a: Int, b: Int) {
self.a = a
self.b = b
}
}
let foo = Foo(a: 1, b: 2)
let dict = try JSONDecoder().decode([String: Int].self, from: JSONEncoder().encode(foo))
print(dict)
let dict = try JSONSerialization.jsonObject(with: try JSONEncoder().encode(struct), options: []) as? [String: Any]
I have modified the PropertyListEncoder from the Swift project into a DictionaryEncoder, simply by removing the final serialisation from dictionary into binary format. You can do the same yourself, or you can take my code from here
It can be used like this:
do {
let employeeDictionary: [String: Any] = try DictionaryEncoder().encode(employee)
} catch let error {
// handle error
}
In some project, i'm used the swift reflection. But be careful, nested codable objects, are not mapped also there.
let dict = Dictionary(uniqueKeysWithValues: Mirror(reflecting: foo).children.map{ ($0.label!, $0.value) })
I definitely think that there's some value in just being able to use Codable to encode to/from dictionaries, without the intention of ever hitting JSON/Plists/whatever. There are plenty of APIs which just give you back a dictionary, or expect a dictionary, and it's nice to be able to interchange them easily with Swift structs or objects, without having to write endless boilerplate code.
I've been playing round with some code based on the Foundation JSONEncoder.swift source (which actually does implement dictionary encoding/decoding internally, but doesn't export it).
The code can be found here: https://github.com/elegantchaos/DictionaryCoding
It's still quite rough, but I've expanded it a bit so that, for example, it can fill in missing values with defaults when decoding.
Here is a protocol based solution:
protocol DictionaryEncodable {
func encode() throws -> Any
}
extension DictionaryEncodable where Self: Encodable {
func encode() throws -> Any {
let jsonData = try JSONEncoder().encode(self)
return try JSONSerialization.jsonObject(with: jsonData, options: .allowFragments)
}
}
protocol DictionaryDecodable {
static func decode(_ dictionary: Any) throws -> Self
}
extension DictionaryDecodable where Self: Decodable {
static func decode(_ dictionary: Any) throws -> Self {
let jsonData = try JSONSerialization.data(withJSONObject: dictionary, options: [])
return try JSONDecoder().decode(Self.self, from: jsonData)
}
}
typealias DictionaryCodable = DictionaryEncodable & DictionaryDecodable
And here is how to use it:
class AClass: Codable, DictionaryCodable {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
struct AStruct: Codable, DictionaryEncodable, DictionaryDecodable {
var name: String
var age: Int
}
let aClass = AClass(name: "Max", age: 24)
if let dict = try? aClass.encode(), let theClass = try? AClass.decode(dict) {
print("Encoded dictionary: \n\(dict)\n\ndata from decoded dictionary: \"name: \(theClass.name), age: \(theClass.age)\"")
}
let aStruct = AStruct(name: "George", age: 30)
if let dict = try? aStruct.encode(), let theStruct = try? AStruct.decode(dict) {
print("Encoded dictionary: \n\(dict)\n\ndata from decoded dictionary: \"name: \(theStruct.name), age: \(theStruct.age)\"")
}
I wrote a quick gist to handle this (not using the Codable protocol). Be careful, it doesn't type-check any values and doesn't work recursively on values that are encodable.
class DictionaryEncoder {
var result: [String: Any]
init() {
result = [:]
}
func encode(_ encodable: DictionaryEncodable) -> [String: Any] {
encodable.encode(self)
return result
}
func encode<T, K>(_ value: T, key: K) where K: RawRepresentable, K.RawValue == String {
result[key.rawValue] = value
}
}
protocol DictionaryEncodable {
func encode(_ encoder: DictionaryEncoder)
}
There no straight forward way of doing this in Codable. You need to implement Encodable/Decodable protocol for your struct. For your example, you might need to write as below
typealias EventDict = [String:Int]
struct Favorite {
var all:EventDict
init(all: EventDict = [:]) {
self.all = all
}
}
extension Favorite: Encodable {
struct FavoriteKey: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: FavoriteKey.self)
for eventId in all {
let nameKey = FavoriteKey(stringValue: eventId.key)!
try container.encode(eventId.value, forKey: nameKey)
}
}
}
extension Favorite: Decodable {
public init(from decoder: Decoder) throws {
var events = EventDict()
let container = try decoder.container(keyedBy: FavoriteKey.self)
for key in container.allKeys {
let fav = try container.decode(Int.self, forKey: key)
events[key.stringValue] = fav
}
self.init(all: events)
}
}
I have made a pod here https://github.com/levantAJ/AnyCodable to facilitate decode and encode [String: Any] and [Any]
pod 'DynamicCodable', '1.0'
And you are able to decode & encode [String: Any] and [Any]
import DynamicCodable
struct YourObject: Codable {
var dict: [String: Any]
var array: [Any]
var optionalDict: [String: Any]?
var optionalArray: [Any]?
enum CodingKeys: String, CodingKey {
case dict
case array
case optionalDict
case optionalArray
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
dict = try values.decode([String: Any].self, forKey: .dict)
array = try values.decode([Any].self, forKey: .array)
optionalDict = try values.decodeIfPresent([String: Any].self, forKey: .optionalDict)
optionalArray = try values.decodeIfPresent([Any].self, forKey: .optionalArray)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(dict, forKey: .dict)
try container.encode(array, forKey: .array)
try container.encodeIfPresent(optionalDict, forKey: .optionalDict)
try container.encodeIfPresent(optionalArray, forKey: .optionalArray)
}
}
After research, we find that if we use the keyword Any in the class which is inherited from the Codable & Decodable it will give the error. So if you want to use a dictionary user with the types of data coming from the server.
For example, the server is sending the dictionary of type [String : Int] then use [String : Int] if you will try [String : Any] it will not work.
Here is dictionary -> object. Swift 5.
extension Dictionary where Key == String, Value: Any {
func object<T: Decodable>() -> T? {
if let data = try? JSONSerialization.data(withJSONObject: self, options: []) {
return try? JSONDecoder().decode(T.self, from: data)
} else {
return nil
}
}
}
Come to think of it, the question does not have an answer in the general case, since the Encodable instance may be something not serializable into a dictionary, such as an array:
let payload = [1, 2, 3]
let encoded = try JSONEncoder().encode(payload) // "[1,2,3]"
Other than that, I have written something similar as a framework.