Storing array of custom objects in UserDefaults - swift

I'm having a heck of a time trying to figure out how to store an array of my custom struct in UserDefaults.
Here is my code:
struct DomainSchema: Codable {
var domain: String
var schema: String
}
var domainSchemas: [DomainSchema] {
get {
if UserDefaults.standard.object(forKey: "domainSchemas") != nil {
let data = UserDefaults.standard.value(forKey: "domainSchemas") as! Data
let domainSchema = try? PropertyListDecoder().decode(DomainSchema.self, from: data)
return domainSchema!
}
return nil
}
set {
UserDefaults.standard.set(try? PropertyListEncoder().encode(newValue), forKey: "domainSchemas")
}
}
struct SettingsView: View {
var body: some View {
VStack {
ForEach(domainSchemas, id: \.domain) { domainSchema in
HStack {
Text(domainSchema.domain)
Text(domainSchema.schema)
}
}
// clear history button
}
.onAppear {
if (domainSchemas.isEmpty) {
domainSchemas.append(DomainSchema(domain: "reddit.com", schema: "apollo://"))
}
}
}
}
It is giving me these errors:
Cannot convert return expression of type 'DomainSchema' to return type '[DomainSchema]'
'nil' is incompatible with return type '[DomainSchema]'
I'm not really sure how to get an array of the objects instead of just a single object, or how to resolve the nil incompatibility error...

If you really want to persist your data using UserDefaults the easiest way would be to use a class and conform it to NSCoding. Regarding your global var domainSchemas I would recommend using a singleton or extend UserDefaults and create a computed property for it there:
class DomainSchema: NSObject, NSCoding {
var domain: String
var schema: String
init(domain: String, schema: String) {
self.domain = domain
self.schema = schema
}
required init(coder decoder: NSCoder) {
self.domain = decoder.decodeObject(forKey: "domain") as? String ?? ""
self.schema = decoder.decodeObject(forKey: "schema") as? String ?? ""
}
func encode(with coder: NSCoder) {
coder.encode(domain, forKey: "domain")
coder.encode(schema, forKey: "schema")
}
}
extension UserDefaults {
var domainSchemas: [DomainSchema] {
get {
guard let data = UserDefaults.standard.data(forKey: "domainSchemas") else { return [] }
return (try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)) as? [DomainSchema] ?? []
}
set {
UserDefaults.standard.set(try? NSKeyedArchiver.archivedData(withRootObject: newValue, requiringSecureCoding: false), forKey: "domainSchemas")
}
}
}
Usage:
UserDefaults.standard.domainSchemas = [.init(domain: "a", schema: "b"), .init(domain: "c", schema: "d")]
UserDefaults.standard.domainSchemas // [{NSObject, domain "a", schema "b"}, {NSObject, domain "c", schema "d"}]
If you prefer the Codable approach persisting the Data using UserDefaults as well:
struct DomainSchema: Codable {
var domain: String
var schema: String
init(domain: String, schema: String) {
self.domain = domain
self.schema = schema
}
}
extension UserDefaults {
var domainSchemas: [DomainSchema] {
get {
guard let data = UserDefaults.standard.data(forKey: "domainSchemas") else { return [] }
return (try? PropertyListDecoder().decode([DomainSchema].self, from: data)) ?? []
}
set {
UserDefaults.standard.set(try? PropertyListEncoder().encode(newValue), forKey: "domainSchemas")
}
}
}
Usage:
UserDefaults.standard.domainSchemas = [.init(domain: "a", schema: "b"), .init(domain: "c", schema: "d")]
UserDefaults.standard.domainSchemas // [{domain "a", schema "b"}, {domain "c", schema "d"}]
I think the best option would be to do not use UserDefaults, create a singleton "shared instance", declare a domainSchemas property there and save your json Data inside a subdirectory of you application support directory:
extension URL {
static var domainSchemas: URL {
let applicationSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let bundleID = Bundle.main.bundleIdentifier ?? "company name"
let subDirectory = applicationSupport.appendingPathComponent(bundleID, isDirectory: true)
try? FileManager.default.createDirectory(at: subDirectory, withIntermediateDirectories: true, attributes: nil)
return subDirectory.appendingPathComponent("domainSchemas.json")
}
}
class Shared {
static let instance = Shared()
private init() { }
var domainSchemas: [DomainSchema] {
get {
guard let data = try? Data(contentsOf: .domainSchemas) else { return [] }
return (try? JSONDecoder().decode([DomainSchema].self, from: data)) ?? []
}
set {
try? JSONEncoder().encode(newValue).write(to: .domainSchemas)
}
}
}
Usage:
Shared.instance.domainSchemas = [.init(domain: "a", schema: "b"), .init(domain: "c", schema: "d")]
Shared.instance.domainSchemas // [{domain "a", schema "b"}, {domain "c", schema "d"}]

You don't need to use NSKeyedArchiver to save custom objects into UserDefaults Because you have to change your struct into a class.
There is an easier solution and That's JSONDecoder and JSONEncoder.
Whenever you want to save a custom object into UserDefaults first convert it into Data by using JSONEncoder and when you want to retrieve an object from Userdefaults you do it by using JSONDecoder. Along with that I highly recommend you to write a separate class or struct to manage your data so that being said you can do:
struct DomainSchema: Codable {
var domain: String
var schema: String
}
struct PersistenceMangaer{
static let defaults = UserDefaults.standard
private init(){}
// save Data method
static func saveDomainSchema(domainSchema: [DomainSchema]){
do{
let encoder = JSONEncoder()
let domainsSchema = try encoder.encode(domainSchema)
defaults.setValue(followers, forKey: "yourKeyName")
}catch let err{
print(err)
}
}
//retrieve data method
static func getDomains() -> [DomainSchema]{
guard let domainSchemaData = defaults.object(forKey: "yourKeyName") as? Data else{return}
do{
let decoder = JSONDecoder()
let domainsSchema = try decoder.decode([DomainSchema].self, from: domainSchemaData)
return domainsSchema
}catch let err{
return([])
}
}
}
Usage:
let domains = PersistenceMangaer.standard.getDomains()
PersistenceMangaer.standard.saveDomainSchema(domainsTosave)

Related

Prevent lost of data on AppStorage when changing a struct

I have a data model that handles a structure and the data the app uses. I'm saving that data using AppStorage.
I recently needed to add an extra value to the struct, and when I did that, all the data saved was gone.
is there any way to prevent this? I can't find anything on Apple's documentation, or other Swift or SwiftUI sites about this.
Here's my data structure and how I save it.
let dateFormatter = DateFormatter()
struct NoteItem: Codable, Hashable, Identifiable {
let id: UUID
var text: String
var date = Date()
var dateText: String {
dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return dateFormatter.string(from: date)
}
var tags: [String] = []
//var starred: Int = 0 // if I add this, it wipes all the data the app has saved
}
final class DataModel: ObservableObject {
#AppStorage("myappdata") public var notes: [NoteItem] = []
init() {
self.notes = self.notes.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
func sortList() {
self.notes = self.notes.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
}
extension Array: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}
I certainly agree that UserDefaults (AppStorage) is no the best choice for this but whatever storage solution you choose you are going to need a migration strategy. So here are two routes you can take to migrate a changed json struct.
The first one is to add a custom init(from:) to your struct and handle the new property separately
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
text = try container.decode(String.self, forKey: .text)
date = try container.decode(Date.self, forKey: .date)
tags = try container.decode([String].self, forKey: .tags)
if let value = try? container.decode(Int.self, forKey: .starred) {
starred = value
} else {
starred = 0
}
}
The other option is to keep the old version of the struct with another name and use it if the decoding fails for the ordinary struct and then convert the result to the new struct
extension NoteItem {
static func decode(string: String) -> [NoteItem]? {
guard let data = string.data(using: .utf8) else { return nil }
if let result = try? JSONDecoder().decode([NoteItem].self, from: data) {
return result
} else if let result = try? JSONDecoder().decode([NoteItemOld].self, from: data) {
return result.map { NoteItem(id: $0.id, text: $0.text, date: $0.date, tags: $0.tags, starred: 0)}
}
return nil
}
}

Swift Codable: Use a parent's key as as value

I have a JSON that has ID's in the root level:
{
"12345": {
"name": "Pim"
},
"54321": {
"name": "Dorien"
}
}
My goal is to use Codable to create an array of User objects that have both name and ID properties.
struct User: Codable {
let id: String
let name: String
}
I know how to use Codable with a single root level key and I know how to work with unknown keys. But what I'm trying to do here is a combination of both and I have no idea what to do next.
Here's what I got so far: (You can paste this in a Playground)
import UIKit
var json = """
{
"12345": {
"name": "Pim"
},
"54321": {
"name": "Dorien"
}
}
"""
let data = Data(json.utf8)
struct User: Codable {
let name: String
}
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode([String: User].self, from: data)
decoded.forEach { print($0.key, $0.value) }
// 54321 User(name: "Dorien")
// 12345 User(name: "Pim")
} catch {
print("Failed to decode JSON")
}
This is what I'd like to do:
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode([User].self, from: data)
decoded.forEach { print($0) }
// User(id: "54321", name: "Dorien")
// User(id: "12345", name: "Pim")
} catch {
print("Failed to decode JSON")
}
Any help is greatly appreciated.
You can use a custom coding key and setup User as below to parse unknown keys,
struct CustomCodingKey: CodingKey {
let intValue: Int?
let stringValue: String
init?(stringValue: String) {
self.intValue = Int(stringValue)
self.stringValue = stringValue
}
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = "\(intValue)"
}
}
struct UserInfo: Codable {
let name: String
}
struct User: Codable {
var id: String = ""
var info: UserInfo?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CustomCodingKey.self)
if let key = container.allKeys.first {
self.id = key.stringValue
self.info = try container.decode(UserInfo.self, forKey: key)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CustomCodingKey.self)
if let key = CustomCodingKey(stringValue: self.id) {
try container.encode(self.info, forKey: key)
}
}
}
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode(User.self, from: data)
print(decoded.id) // 12345
print(decoded.info!.name) // Pim
} catch {
print("Failed to decode JSON")
}

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

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)

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")
}
}

Can Swift convert a class / struct data into dictionary?

For example:
class Test {
var name: String;
var age: Int;
var height: Double;
func convertToDict() -> [String: AnyObject] { ..... }
}
let test = Test();
test.name = "Alex";
test.age = 30;
test.height = 170;
let dict = test.convertToDict();
dict will have content:
{"name": "Alex", "age": 30, height: 170}
Is this possible in Swift?
And can I access a class like a dictionary, for example probably using:
test.value(forKey: "name");
Or something like that?
You can just add a computed property to your struct to return a Dictionary with your values. Note that Swift native dictionary type doesn't have any method called value(forKey:). You would need to cast your Dictionary to NSDictionary:
struct Test {
let name: String
let age: Int
let height: Double
var dictionary: [String: Any] {
return ["name": name,
"age": age,
"height": height]
}
var nsDictionary: NSDictionary {
return dictionary as NSDictionary
}
}
You can also extend Encodable protocol as suggested at the linked answer posted by #ColGraff to make it universal to all Encodable structs:
struct JSON {
static let encoder = JSONEncoder()
}
extension Encodable {
subscript(key: String) -> Any? {
return dictionary[key]
}
var dictionary: [String: Any] {
return (try? JSONSerialization.jsonObject(with: JSON.encoder.encode(self))) as? [String: Any] ?? [:]
}
}
struct Test: Codable {
let name: String
let age: Int
let height: Double
}
let test = Test(name: "Alex", age: 30, height: 170)
test["name"] // Alex
test["age"] // 30
test["height"] // 170
You could use Reflection and Mirror like this to make it more dynamic and ensure you do not forget a property.
struct Person {
var name:String
var position:Int
var good : Bool
var car : String
var asDictionary : [String:Any] {
let mirror = Mirror(reflecting: self)
let dict = Dictionary(uniqueKeysWithValues: mirror.children.lazy.map({ (label:String?, value:Any) -> (String, Any)? in
guard let label = label else { return nil }
return (label, value)
}).compactMap { $0 })
return dict
}
}
let p1 = Person(name: "Ryan", position: 2, good : true, car:"Ford")
print(p1.asDictionary)
["name": "Ryan", "position": 2, "good": true, "car": "Ford"]
A bit late to the party, but I think this is great opportunity for JSONEncoder and JSONSerialization.
The accepted answer does touch on this, this solution saves us calling JSONSerialization every time we access a key, but same idea!
extension Encodable {
/// Encode into JSON and return `Data`
func jsonData() throws -> Data {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .iso8601
return try encoder.encode(self)
}
}
You can then use JSONSerialization to create a Dictionary if the Encodable should be represented as an object in JSON (e.g. Swift Array would be a JSON array)
Here's an example:
struct Car: Encodable {
var name: String
var numberOfDoors: Int
var cost: Double
var isCompanyCar: Bool
var datePurchased: Date
var ownerName: String? // Optional
}
let car = Car(
name: "Mazda 2",
numberOfDoors: 5,
cost: 1234.56,
isCompanyCar: true,
datePurchased: Date(),
ownerName: nil
)
let jsonData = try car.jsonData()
// To get dictionary from `Data`
let json = try JSONSerialization.jsonObject(with: jsonData, options: [])
guard let dictionary = json as? [String : Any] else {
return
}
// Use dictionary
guard let jsonString = String(data: jsonData, encoding: .utf8) else {
return
}
// Print jsonString
print(jsonString)
Output:
{
"numberOfDoors" : 5,
"datePurchased" : "2020-03-04T16:04:13Z",
"name" : "Mazda 2",
"cost" : 1234.5599999999999,
"isCompanyCar" : true
}
Use protocol, it is an elegant solution. 1. encode struct or class to data 2. decode data and transfer to dictionary.
/// define protocol convert Struct or Class to Dictionary
protocol Convertable: Codable {
}
extension Convertable {
/// implement convert Struct or Class to Dictionary
func convertToDict() -> Dictionary<String, Any>? {
var dict: Dictionary<String, Any>? = nil
do {
print("init student")
let encoder = JSONEncoder()
let data = try encoder.encode(self)
print("struct convert to data")
dict = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? Dictionary<String, Any>
} catch {
print(error)
}
return dict
}
}
struct Student: Convertable {
var name: String
var age: Int
var classRoom: String
init(_ name: String, age: Int, classRoom: String) {
self.name = name
self.age = age
self.classRoom = classRoom
}
}
let student = Student("zgpeace", age: 18, classRoom: "class one")
print(student.convertToDict() ?? "nil")
ref: https://a1049145827.github.io/2018/03/02/Swift-%E4%BB%8E%E9%9B%B6%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AAStruct%E6%88%96Class%E8%BD%ACDictionary%E7%9A%84%E9%9C%80%E6%B1%82/
This answer is like the above which uses Mirror. But consider the nested class/struct case.
extension Encodable {
func dictionary() -> [String:Any] {
var dict = [String:Any]()
let mirror = Mirror(reflecting: self)
for child in mirror.children {
guard let key = child.label else { continue }
let childMirror = Mirror(reflecting: child.value)
switch childMirror.displayStyle {
case .struct, .class:
let childDict = (child.value as! Encodable).dictionary()
dict[key] = childDict
case .collection:
let childArray = (child.value as! [Encodable]).map({ $0.dictionary() })
dict[key] = childArray
case .set:
let childArray = (child.value as! Set<AnyHashable>).map({ ($0 as! Encodable).dictionary() })
dict[key] = childArray
default:
dict[key] = child.value
}
}
return dict
}
}