Store custom data type in #AppStorage with optional initializer? - swift

I'm trying to store a custom data type into AppStorage. To do so, the model conforms to RawRepresentable (followed this tutorial). It's working fine, but when I initialize the #AppStorage variable, it requires an initial UserModel value. I want to make the variable optional, so it can be nil if the user is signed out. Is this possible?
Within a class / view, I can init like this:
#AppStorage("user_model") private(set) var user: UserModel = UserModel(id: "", name: "", email: "")
But I want to init like this:
#AppStorage("user_model") private(set) var user: UserModel?
Model:
struct UserModel: Codable {
let id: String
let name: String
let email: String
enum CodingKeys: String, CodingKey {
case id
case name
case email
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
do {
id = try String(values.decode(Int.self, forKey: .id))
} catch DecodingError.typeMismatch {
id = try String(values.decode(String.self, forKey: .id))
}
self.name = try values.decode(String.self, forKey: .name)
self.email = try values.decode(String.self, forKey: .email)
}
init(id: String, name: String, email: String) {
self.id = id
self.name = name
self.email = email
}
}
// MARK: RAW REPRESENTABLE
extension UserModel: RawRepresentable {
// RawRepresentable allows a UserModel to be store in AppStorage directly.
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(UserModel.self, from: data)
else {
return nil
}
self = result
}
var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(email, forKey: .email)
}
}

The code below works because you added a conformance UserModel: RawRepresentable:
#AppStorage("user_model") private(set) var user: UserModel = UserModel(id: "", name: "", email: "")
You need to do the same for UserModel? if you want the following to work:
#AppStorage("user_model") private(set) var user: UserModel? = nil
Here is a possible solution:
extension Optional: RawRepresentable where Wrapped == UserModel {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(UserModel.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
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: UserModel.CodingKeys.self)
try container.encode(self?.id, forKey: .id)
try container.encode(self?.name, forKey: .name)
try container.encode(self?.email, forKey: .email)
}
}
Note: I reused the implementation you already had for UserModel: RawRepresentable - it might need some corrections for this case.
Also because you conform Optional: RawRepresentable you need to make
UserModel public as well.

A possible generic approach for any optional Codable:
extension Optional: RawRepresentable where Wrapped: Codable {
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let json = String(data: data, encoding: .utf8)
else {
return "{}"
}
return json
}
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let value = try? JSONDecoder().decode(Self.self, from: data)
else {
return nil
}
self = value
}
}
With that in place, any Codable can now be persisted in app storage:
#AppStorage("user_model") var user: UserModel? = nil

Related

Decoding JSON in Swift to Dictionary with different types

I have a JSON result from the server that looks like the following:
let json = """
{
"type": "rating",
"data": {
"maxRating": 5,
"isDarkMode": true
}
}
"""
The value for data can be any key-values. I want to map this JSON to my Swift model. So, I implemented the following:
struct Model: Decodable {
let type: String
let data: [String: Any]
private enum CodingKeys: String, CodingKey {
case type
case data
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.type = try container.decode(String.self, forKey: .type)
self.data = try container.decode([String: Any].self, forKey: .data) // THIS
}
}
But on self.data = try container.decode ... it gives me the following error:
error: no exact matches in call to instance method 'decode'
self.data = try container.decode([String: Any].self, forKey: .data)
How can I fix it?
[String : Any] is not decodable. You can not decode with Any. But there have other solutions. Here is one https://dev.to/absoftware/how-to-make-swift-s-string-any-decodable-5c6n
Make change in the Metadata as -
enum DataType: String, Codable {
case payload
case metadata
}
struct Payload: Codable {
let id: String
let eventName: String
let metadata: [Metadata]
}
struct Metadata: Codable {
let maxRating: Int?
let isDarkMode: Bool?
// Add other variables that may appear from your JSON
}
enum MyValue: Decodable {
case payload(_ payload: Payload)
case metadata(_ metadata: Metadata)
private enum CodingKeys: String, CodingKey {
case `type`
case `data`
}
init(from decoder: Decoder) throws {
let map = try decoder.container(keyedBy: CodingKeys.self)
let dataType = try map.decode(DataType.self, forKey: .type)
switch dataType {
case .payload:
self = .payload(try map.decode(Payload.self, forKey: .data))
case .metadata:
self = .metadata(try map.decode(Metadata.self, forKey: .data))
}
}
}
Use the following Extension
JSONCodingKeys.swift
import Foundation
// Inspired by https://gist.github.com/mbuchetics/c9bc6c22033014aa0c550d3b4324411a
struct JSONCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}
extension KeyedDecodingContainer {
func decode(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any> {
let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any>? {
guard contains(key) else {
return nil
}
return try decode(type, forKey: key)
}
func decode(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any> {
var container = try self.nestedUnkeyedContainer(forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any>? {
guard contains(key) else {
return nil
}
return try decode(type, forKey: key)
}
func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
var dictionary = Dictionary<String, Any>()
for key in allKeys {
if let boolValue = try? decode(Bool.self, forKey: key) {
dictionary[key.stringValue] = boolValue
} else if let stringValue = try? decode(String.self, forKey: key) {
dictionary[key.stringValue] = stringValue
} else if let intValue = try? decode(Int.self, forKey: key) {
dictionary[key.stringValue] = intValue
} else if let doubleValue = try? decode(Double.self, forKey: key) {
dictionary[key.stringValue] = doubleValue
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedDictionary
} else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedArray
}
}
return dictionary
}
}
extension UnkeyedDecodingContainer {
mutating func decode(_ type: Array<Any>.Type) throws -> Array<Any> {
var array: [Any] = []
while isAtEnd == false {
if let value = try? decode(Bool.self) {
array.append(value)
} else if let value = try? decode(Double.self) {
array.append(value)
} else if let value = try? decode(String.self) {
array.append(value)
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self) {
array.append(nestedDictionary)
} else if let nestedArray = try? decode(Array<Any>.self) {
array.append(nestedArray)
}
}
return array
}
mutating func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self)
return try nestedContainer.decode(type)
}
}
How to use.
struct Model: Decodable {
let type: String
let data: [String: Any]
private enum CodingKeys: String, CodingKey {
case type
case data
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.type = try container.decode(String.self, forKey: .type)
self.data = try container.decode([String: Any].self, forKey: .data)
}
}
let decoder = JSONDecoder()
let userJson = """
{
"type": "rating",
"data": {
"maxRating": 5,
"isDarkMode": true
}
}
""".data(using: .utf8)!
let user = try! decoder.decode(Model.self, from: userJson)
print(user)
Original answer from
You have to create different struct for data parameter.
You can refer below struct for reference
import Foundation
struct JsonResponse : Codable {
let type : String?
let data : Data?
enum CodingKeys: String, CodingKey {
case type = "type"
case data = "data"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
type = try values.decodeIfPresent(String.self, forKey: .type)
data = try values.decodeIfPresent(Data.self, forKey: .data)
}
}
struct Data : Codable {
let maxRating : Int?
let isDarkMode : Bool?
enum CodingKeys: String, CodingKey {
case maxRating = "maxRating"
case isDarkMode = "isDarkMode"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
maxRating = try values.decodeIfPresent(Int.self, forKey: .maxRating)
isDarkMode = try values.decodeIfPresent(Bool.self, forKey: .isDarkMode)
}
}

Save array of different object types in UserDefaults

I have two classes, Radio and Podcast, children of the Media class.
I'm trying to save an array of Media (with radios and podcasts) to UserDefaults but when I get it back, I only have medias (I'm losing information of Radio or Podcast).
I cannot cast the items to Radio or Podcast.
private func saveRecentMediaInData(_ medias:[Media]) {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(medias) {
UserDefaults.standard.setValue(encoded, forKey: recentMediasKey)
}
}
private func getRecentMediasFromData() -> [Media] {
let defaults = UserDefaults.standard
if let data = defaults.value(forKey: recentMediasKey) as? Data {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode(Array.self, from: data) as [Media] {
return decoded
}
}
return []
}
Thanks
The issue is not related to UserDefaults. It's having an array of mixed object to decode with Codable.
In this case, a solution is to use a enum with associated value:
enum Mixed: Codable {
case radio(Radio)
case podcast(Podcast)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let asRadio = try? container.decode(Radio.self) {
self = .radio(asRadio)
} else if let asPodcast = try? container.decode(Podcast.self) {
self = .podcast(asPodcast)
} else {
fatalError("Oops")
}
}
}
Here is a full sample code:
struct SubClassesCodable {
class Media: Codable, CustomStringConvertible {
var title: String
var description: String {
return "Media: \(title)"
}
}
class Radio: Media {
var channel: Int
enum CodingKeys: String, CodingKey {
case channel
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.channel = try container.decode(Int.self, forKey: .channel)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(channel, forKey: .channel)
}
override var description: String {
return "Radio: \(title) - \(channel)"
}
}
class Podcast: Media {
var author: String
enum CodingKeys: String, CodingKey {
case author
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.author = try container.decode(String.self, forKey: .author)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(author, forKey: .author)
}
override var description: String {
return "Podcast: \(title) - \(author)"
}
}
enum Mixed: Codable {
case radio(Radio)
case podcast(Podcast)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let asRadio = try? container.decode(Radio.self) {
self = .radio(asRadio)
} else if let asPodcast = try? container.decode(Podcast.self) {
self = .podcast(asPodcast)
} else {
fatalError("Oops") //Or rather throws a custom error
}
}
}
static func test() {
let mediaJSONStr = #"{"title": "media"}"#
let radioJSONStr = #"{"title": "radio", "channel": 3}"#
let podcastJSONStr = #"{"title": "podcast", "author": "myself"}"#
do {
let decoder = JSONDecoder()
//Create values from JSON
let media = try decoder.decode(Media.self, from: Data(mediaJSONStr.utf8))
print(media)
let radio = try decoder.decode(Radio.self, from: Data(radioJSONStr.utf8))
print(radio)
let podcast = try decoder.decode(Podcast.self, from: Data(podcastJSONStr.utf8))
print(podcast)
let array: [Media] = [radio, podcast]
print(array)
// Encode to Data, that's what's saved into UserDefaults
let encoder = JSONEncoder()
let encodedArray = try encoder.encode(array)
print("Encoded: \(String(data: encodedArray, encoding: .utf8)!)") //It's more readable as JSON String than Data
//This will fail, it's the current author code
let decoded = try decoder.decode([Media].self, from: encodedArray)
print(decoded)
decoded.forEach {
if let asRadio = $0 as? Radio {
print(asRadio)
}else if let asPodcast = $0 as? Podcast {
print(asPodcast)
} else {
print("Nop: \($0)")
}
}
//This is a working solution
let mixedDecoded = try decoder.decode([Mixed].self, from: encodedArray)
let decodedArray: [Media] = mixedDecoded.map {
switch $0 {
case .radio(let radio):
return radio
case .podcast(let podcast):
return podcast
}
}
print(decodedArray)
} catch {
print("Error: \(error)")
}
}
}
SubClassesCodable.test()

Swift - Decode/encode an array of generics with different types

How can I decode/encode an array of different generic types?
I have a data structure, which has properties, that conform to a protocol Connection, thus I use generics:
// Data structure which saves two objects, which conform to the Connection protocol
struct Configuration<F: Connection, T: Connection>: Codable {
var from: F
var to: T
private var id: String = UUID.init().uuidString
enum CodingKeys: String, CodingKey {
case from, to, id
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.from = try container.decode(F.self, forKey: .from)
self.to = try container.decode(T.self, forKey: .to)
self.id = try container.decode(String.self, forKey: .id)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(from, forKey: .from)
try container.encode(to, forKey: .to)
try container.encode(id, forKey: .id)
}
}
protocol Connection: Codable {
var path: String { get set }
}
// Two implementations of the Connection protocol
struct SFTPConnection: Connection, Codable {
var path: String
var user: String
var sshKey: String
}
struct FTPConnection: Connection, Codable {
var path: String
var user: String
var password: String
}
This works fine when I know of what type the connections F and T are. But I have cases, where I want to load a configuration, not knowing which type F and T are.
public static func load<F: Connection, T: Connection>(for key: String) throws -> Configuration<F, T>? {
// Load from UserDefaults
guard let configurationData = defaults.object(forKey: key) as? Data else {
return nil
}
// Decode
guard let configuration = try? PropertyListDecoder().decode(Configuration<F, T>.self, from: configurationData) else {
return nil
}
return configuration
}
// OR
func loadAll<F:Connection, T: Connection>() -> [String: Configuration<F, T>]? {
return UserDefaults.standard.dictionaryRepresentation() as? [String: Configuration<F, T>]
}
In the above cases F and T could be of any unknown type, that conforms to the Connection protocol. So the above functions wouldn't work, since I would need to specify a specific type for F and T when calling the function, which I don't know.
In the second function, F alone could actually be of different types. That's where it gets difficult. I figured I need to somehow store the types of F and T in the User Defaults as well and then use them in the decode and encode function (thus discarding the generics). But I have no idea how I would elegantly do that.
I would appreciate any ideas on how to solve this problem!
The following solutions resolves all the issues, that I had with generics and not knowing the specific type of Connection. The key to the solution was
saving the type of a Connection implementation in the implementation itself and
Using superEncoder and superDecoder to encode/decode the from and to properties.
This is the solution:
import Foundation
protocol Connection: Codable {
var type: ConnectionType { get }
var path: String { get set }
}
struct LocalConnection: Connection {
let type: ConnectionType = ConnectionType.local
var path: String
}
struct SFTPConnection : Connection {
let type: ConnectionType = ConnectionType.sftp
var path: String
var user: String
var sshKey: String
init(path: String, user: String, sshKey: String) {
self.path = path
self.user = user
self.sshKey = sshKey
}
}
struct FTPConnection: Connection {
let type: ConnectionType = ConnectionType.ftp
var path: String
var user: String
var password: String
}
struct TFTPConnection: Connection {
let type: ConnectionType = ConnectionType.tftp
var path: String
}
enum ConnectionType : Int, Codable {
case local
case sftp
case ftp
case tftp
func getType() -> Connection.Type {
switch self {
case .local: return LocalConnection.self
case .sftp: return SFTPConnection.self
case .ftp: return FTPConnection.self
case .tftp: return TFTPConnection.self
}
}
}
struct Configuration {
var from : Connection
var to : Connection
private var id = UUID.init().uuidString
var fromType : ConnectionType { return from.type }
var toType : ConnectionType { return to.type }
init(from: Connection, to: Connection) {
self.from = from
self.to = to
}
}
extension Configuration : Codable {
enum CodingKeys: String, CodingKey {
case id, from, to, fromType, toType
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
var type : ConnectionType
type = try container.decode(ConnectionType.self, forKey: .fromType)
let fromDecoder = try container.superDecoder(forKey: .from)
self.from = try type.getType().init(from: fromDecoder)
type = try container.decode(ConnectionType.self, forKey: .toType)
let toDecoder = try container.superDecoder(forKey: .to)
self.to = try type.getType().init(from: toDecoder)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.fromType, forKey: .fromType)
let fromContainer = container.superEncoder(forKey: .from)
try from.encode(to: fromContainer)
try container.encode(self.toType, forKey: .toType)
let toContainer = container.superEncoder(forKey: .to)
try to.encode(to: toContainer)
}
}

How to encode/decode a dictionary with Codable values for storage in UserDefaults?

I am trying to store a dictionary of company names (string) mapped to Company objects (from a struct Company) in iOS UserDefaults. I have created the Company struct and made it conform to Codable. I have one example a friend helped me with in my project where we created a class Account and stored it in UserDefaults by making a Defaults struct (will include example code). I have read in the swift docs that dictionaries conform to Codable and in order to stay Codable, must contain Codable objects. That is why I made struct Company conform to Codable.
I have created a struct for Company that conforms to Codable. I have tried using model code to create a new struct CompanyDefaults to handle the getting and setting of the Company dictionary from/to UserDefaults. I feel I have some beginner misconceptions about what needs to happen and about how it should be implemented (with good design in mind).
The dictionary I wish to store looks like [String:Company]
where company name will be String and a Company object for Company
I used conform to Codable as I did some research and it seemed like a newer method for completing similar tasks.
Company struct
struct Company: Codable {
var name:String?
var initials:String? = nil
var logoURL:URL? = nil
var brandColor:String? = nil // Change to UIColor
enum CodingKeys:String, CodingKey {
case name = "name"
case initials = "initials"
case logoURL = "logoURL"
case brandColor = "brandColor"
}
init(name:String?, initials:String?, logoURL:URL?, brandColor:String?) {
self.name = name
self.initials = initials
self.logoURL = logoURL
self.brandColor = brandColor
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
initials = try values.decode(String.self, forKey: .initials)
logoURL = try values.decode(URL.self, forKey: .logoURL)
brandColor = try values.decode(String.self, forKey: .brandColor)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(initials, forKey: .initials)
try container.encode(logoURL, forKey: .logoURL)
try container.encode(brandColor, forKey: .brandColor)
}
}
Defaults struct to control storage
struct CompanyDefaults {
static private let companiesKey = "companiesKey"
static var companies: [String:Company] = {
guard let data = UserDefaults.standard.data(forKey: companiesKey) else { return [:] }
let companies = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [String : Company] ?? [:]
return companies!
}() {
didSet {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: companies, requiringSecureCoding: false) else {
return
}
UserDefaults.standard.set(data, forKey: companiesKey)
}
}
}
I should be able to reference the stored dictionary throughout my code like CompanyDefaults.companies.count
For reference, a friend helped me do a similar task for an array of Account classes stored in user defaults. The code that works perfectly for that is below. The reason I tried a different way is that I had a different data structure (dictionary) and made the decision to use structs.
class Account: NSObject, NSCoding {
let service: String
var username: String
var password: String
func encode(with aCoder: NSCoder) {
aCoder.encode(service)
aCoder.encode(username)
aCoder.encode(password)
}
required init?(coder aDecoder: NSCoder) {
guard let service = aDecoder.decodeObject() as? String,
var username = aDecoder.decodeObject() as? String,
var password = aDecoder.decodeObject() as? String else {
return nil
}
self.service = service
self.username = username
self.password = password
}
init(service: String, username: String, password: String) {
self.service = service
self.username = username
self.password = password
}
}
struct Defaults {
static private let accountsKey = "accountsKey"
static var accounts: [Account] = {
guard let data = UserDefaults.standard.data(forKey: accountsKey) else { return [] }
let accounts = NSKeyedUnarchiver.unarchiveObject(with: data) as? [Account] ?? []
return accounts
}() {
didSet {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: accounts, requiringSecureCoding: false) else {
return
}
UserDefaults.standard.set(data, forKey: accountsKey)
}
}
}
You are mixing up NSCoding and Codable. The former requires a subclass of NSObject, the latter can encode the structs and classes directly with JSONEncoder or ProperListEncoder without any Keyedarchiver which also belongs to NSCoding.
Your struct can be reduced to
struct Company: Codable {
var name : String
var initials : String
var logoURL : URL?
var brandColor : String?
}
That's all, the CodingKeys and the other methods are synthesized. I would at least declare name and initials as non-optional.
To read and save the data is pretty straightforward. The corresponding CompanyDefaults struct is
struct CompanyDefaults {
static private let companiesKey = "companiesKey"
static var companies: [String:Company] = {
guard let data = UserDefaults.standard.data(forKey: companiesKey) else { return [:] }
return try? JSONDecoder.decode([String:Company].self, from: data) ?? [:]
}() {
didSet {
guard let data = try? JSONEncoder().encode(companies) else { return }
UserDefaults.standard.set(data, forKey: companiesKey)
}
}
}

How to encode Realm's List<> type

I am trying to encode my Realm database to JSON. Everything is working except the List<> encoding. So my question is, how would you encode List<>? Because the List doesn't conform to Encodable neighter Decodable protocol.
Right now I am doing this:
#objcMembers class User: Object, Codable{
dynamic var name: String = ""
let dogs = List<Dog>()
private enum UserCodingKeys: String, CodingKey {
case name
case dogs
}
convenience init(name: String) {
self.init()
self.name = name
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: UserCodingKeys.self)
try container.encode(name, forKey: .name)
}
#objcMembers class Dog: Object, Codable{
dynamic var name: String = ""
dynamic var user: User? = nil
private enum DogCodingKeys: String, CodingKey {
case name
}
convenience init(name: String) {
self.init()
name = name
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: DogCodingKeys.self)
try container.encode(name, forKey: .name)
}
}
and like this I am trying to do it:
var json: Any?
let user = RealmService.shared.getUsers()
var usersArray = [User]()
for user in users{
usersArray.append(user)
}
let jsonEncoder = JSONEncoder()
let jsonDecoder = JSONDecoder()
let encodedJson = try? jsonEncoder.encode(portfoliosArray)
if let data = encodedJson {
json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
if let json = json {
print(String(describing: json))
}
}
So the question is how I am able to encode the List<Dog>?
To make a Realm object model class with a property of type List conform to Encodable, you can simply convert the List to an Array in the encode(to:) method, which can be encoded automatically.
extension User: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.username, forKey: .username)
let dogsArray = Array(self.dogs)
try container.encode(dogsArray, forKey: .dogs)
}
}
Test classes I used (slightly different from the ones in your question, but I already had these on hand and the methods in question will be almost identical regardless of the variable names):
class Dog: Object,Codable {
#objc dynamic var id:Int = 0
#objc dynamic var name:String = ""
}
class User: Object, Decodable {
#objc dynamic var id:Int = 0
#objc dynamic var username:String = ""
#objc dynamic var email:String = ""
let dogs = List<Dog>()
private enum CodingKeys: String, CodingKey {
case id, username, email, dogs
}
required convenience init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
username = try container.decode(String.self, forKey: .username)
email = try container.decode(String.self, forKey: .email)
let dogsArray = try container.decode([Dog].self, forKey: .dogs)
dogs.append(objectsIn: dogsArray)
}
}
Test the encoding/decoding:
let userJSON = """
{
"id":1,
"username":"John",
"email":"example#ex.com",
"dogs":[
{"id":2,"name":"King"},
{"id":3,"name":"Kong"}
]
}
"""
do {
let decodedUser = try JSONDecoder().decode(User.self, from: userJSON.data(using: .utf8)!)
let encodedUser = try JSONEncoder().encode(decodedUser)
print(String(data: encodedUser, encoding: .utf8)!)
} catch {
print(error)
}
Output:
{"username":"John","dogs":[{"id":2,"name":"King"},{"id":3,"name":"Kong"}]}
You could resort to a mini-hack by making List conform to Encodable:
extension List: Encodable {
public func encode(to coder: Encoder) throws {
// by default List is not encodable, throw an exception
throw NSError(domain: "SomeDomain", code: -1, userInfo: nil)
}
}
// let's ask it to nicely encode when Element is Encodable
extension List where Element: Encodable {
public func encode(to coder: Encoder) throws {
var container = coder.unkeyedContainer()
try container.encode(contentsOf: self)
}
}
Two extensions are needed as you can't add protocol conformance and where clauses at the same time.
Also note that this approach doesn't provide compile-time checks - e.g. a List<Cat> will throw an exception an runtime if Cat is not encodable, instead of a nice compile time error.
The upside is lot of boilerplate code no longer needed:
#objcMembers class User: Object, Encodable {
dynamic var name: String = ""
let dogs = List<Dog>()
convenience init(name: String) {
self.init()
self.name = name
}
}
#objcMembers class Dog: Object, Encodable {
dynamic var name: String = ""
dynamic var user: User? = nil
convenience init(name: String) {
self.init()
name = name
}
}
This is also scalable, as adding new classes don't require any encoding code, but with the mentioned downside of not being fully type safe at compile time.