I have a program with a class hierarchy similar to this (only much larger, more nested and more complicated)
class A {
let b: [B]
}
class B {
}
class B1: B {
/ / ....
}
class B2: B {
/ / ....
}
Class A essentially has an array of instances of B1 and B2 mixed together. If I want to decode class A, I can't just tell it to decode the array as [B].self, as that will call B.init(from: Decoder) which doesn't know how to properly instantiate the B1 and B2 instances.
Right now I'm thinking of a way to wrap these instances and include the class names during the encoding process, so that I can switch over the names later and manually decide which class I want to instantiate. However, as I said, the real hierarchy is considerably more complicated, and so doing this in all instances would be a real chore.
So my question is this: is there any simple way of encoding and decoding this structure that I'm not thinking of? Or does anyone have a better idea?
Quick update: my idea of wrapping the classes didn't work. I also tried encoding the classes into dictionaries I could deal with manually, but it seems [String: Codable] doesn't actually conform to Codable, so I'm at a loss here. So, yeah, any help would be appreciated.
I think you need factory initializers. Current Swift, unlike Objective-C does not support factory initializers but there is a workaround. There is a limitation however, you can only use factory initializer in initializers declared as convenience, which means you can't override it in subclasses. Your code would be something like this:
Depending on your use case, you could use an enum to wrap it.
class A: Codable {
let bValues: [BWrapper]
init(bValues: [BWrapper]) {
self.bValues = bValues
}
}
class B: Codable {
let name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "B")
}
}
class B1: B {
var otherProperty: String
init() {
self.otherProperty = "Other"
super.init(name: "B1")
}
required init(from decoder: Decoder) throws {
// We only need to manually deal with the subclass's properties
self.otherProperty = try decoder.container(keyedBy: CodingKeys.self)
.decode(String.self, forKey: .otherProperty)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = try encoder.container(keyedBy: CodingKeys.self)
// We only need to manually deal with the subclass's properties
try container.encode(otherProperty, forKey: .otherProperty)
}
enum CodingKeys: String, CodingKey {
case otherProperty
}
}
class B2: B {
init() {
super.init(name: "B2")
}
required init(from decoder: Decoder) throws {
//If this class has additional properties, you will probably need to handle that here
try super.init(from: decoder)
}
}
enum BWrapper: Codable {
enum ErrorTypes: LocalizedError {
case invalidType
var localizedDescription: String {
switch self {
case .invalidType:
return "Impropper Type of b passed when attemption to decode"
}
}
}
case plainType(B)
case firstType(B1)
case secondType(B2)
var caseKey: String {
switch self {
case .plainType(_): return "plainType"
case .firstType(_): return "firstType"
case .secondType(_): return "secondType"
}
}
var bValue: B {
switch self {
case .plainType(let b): return b
case .firstType(let b): return b
case .secondType(let b): return b
}
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let caseKey = try values.decode(String.self, forKey: .caseKey)
switch caseKey {
case "plainType":
let value = try values.decode(B.self, forKey: .value)
self = .plainType(value)
case "firstType":
let value = try values.decode(B1.self, forKey: .value)
self = .firstType(value)
case "secondType":
let value = try values.decode(B2.self, forKey: .value)
self = .secondType(value)
default:
throw ErrorTypes.invalidType
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(caseKey, forKey: .caseKey)
switch self {
case .plainType(let b):
try container.encode(b, forKey: .value)
case .firstType(let b):
try container.encode(b, forKey: .value)
case .secondType(let b):
try container.encode(b, forKey: .value)
}
}
enum CodingKeys: String, CodingKey {
case caseKey
case value
}
}
let testItem = A(bValues: [.plainType(B()), .firstType(B1()), .secondType(B2())])
let jsonData = try! JSONEncoder().encode(testItem)
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString ?? "" )
/*
prints:
{"bValues":[{"caseKey":"plainType","value":{"name":"B"}},{"caseKey":"firstType","value":{"name":"B1","otherProperty":"Other"}},{"caseKey":"secondType","value":{"name":"B2"}}]}
*/
let convertedType = try! JSONDecoder().decode(A.self, from: jsonData)
convertedType.bValues.forEach{ print($0.bValue.name) }
/*
prints:
B
B1
B2
*/
Admittedly it's a bit messy, but could probably be cleaned up a bit. I'm guessing there's a protocol that could be abstracted from the enum to handle some of the functionality.
Thanks for all your answers! They really helped me get there.
In the end I did something similar to @GetSwifty's suggestion, except my BWrapper was a class instead of an enum. I think there are probably advantages to using an enum, but this project demanded a class and everything turned out fine