|
Hi All,
I'm looking at Composition over Inheritance but am running into problems trying to decode, as it appears to need concrete types (even if the the protocols themselves conform)?
A quick outline...very loose, I know, but should suggest what I'm after
enum EnumSubItemType {
case subItem01, subItem01
}
protocol SubItemProtocol: Codable {
}
struct SubItemModel01: SubItemProtocol {
// Necessary bits (inits, encoders, decoders, etc)
}
struct SubItemModel02: SubItemProtocol {
// As Above
}
class MainItem: Codable {
var name: String
var subItemType: EnumSubItemType
var subItem: SubItemProtocol
enum CodingKeys: String, CodingKey {
case name
case subItemType
case subItem
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.name = try values.decode(String.self, forKey: .name)
self.subItemType = try values.decode(String.self, forKey: .subItemType)
let subItemContainer = try values.superDecoder(forKey: .subItem)
let subItemDecoder = try values.superDecoder(forKey: .subItem)
switch self.objectType {
case .subItem01:
self.subItem = try subItemDecoder.decode(SubItem01.self, forKey: .subItem)
// Other cases
default:
self.subItem = nil
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(subItemType, forKey: .subItemType)
try subItem?.encode(to: container.superEncoder())
}
}
}
(Please note, the above is not meant to be a fully working version, of course :) )
When trying to decode, the compiler throws up the errors:
- Cannot infer contextual base in reference to member 'subItem'
- Value of type 'any Decoder' has no member 'decode'
on the line 'self.subItem = try subItemDecoder.decode(SubItem01.self, forKey: .subItem)'
I hope the above makes sense, and if anyone could shed any light, that would be really useful, as it seems a good way to go (I can use multi-inheritance but am trying to expand my understanding).
Thanks in advance,
Jes
|
|
It would really, really help if you gave us an example of the JSON you want to work with.
|
|
@roosterboy:
It would really, really help if you gave us an example of the JSON you want to work with.
Ok, here is an example of what I mean:
{
"weapons" : [
{
"id": 1,
"name": "Dagger 01",
"objectType": 1,
"bladeLength": 5
},
{
"id": 2,
"name": "Sword 01",
"objectType": 2,
"swingFatigue": 3
},
{
"id": 3,
"name": "Crossbow 01",
"objectType": 3,
"reloadTime": 25
}
]
}
So, yes, I could code it like this:
class Weapon: Codable {
let weapons: [WeaponElement]
init(weapons: [WeaponElement]) {
self.weapons = weapons
}
}
class WeaponElement: Codable {
let id: Int
let name: String
let objectType: Int
let bladeLength, swingFatigue, reloadTime: Int?
init(id: Int, name: String, objectType: Int, bladeLength: Int?, swingFatigue: Int?, reloadTime: Int?) {
self.id = id
self.name = name
self.objectType = objectType
self.bladeLength = bladeLength
self.swingFatigue = swingFatigue
self.reloadTime = reloadTime
}
}
But that's not what I want, as instead of the optional single values (bladeLength, etc) there will be multiple fields for each weapon (again, this is just for demonstrative purposes). I want to be able to do something like:
enum ObjectType {
case dagger, sword, crossbow
}
protocol subItemProtocol {
}
struct Dagger: subItemProtocol {
var bladeLength: Int
}
struct Sword: subItemProtocol {
var swingFatigue: Int
}
struct Crossbow: subItemProtocol {
var reloadTime: Int
}
class Weapon: Codable {
let weapons: [WeaponElement]
init(weapons: [WeaponElement]) {
self.weapons = weapons
}
}
class WeaponElement: Codable {
let id: Int
let name: String
let objectType: ObjectType
let subItem: subItemProtocol
init(forWeaponAsDagger id: Int, name: String, bladeLength: Int = 5) {
self.id = id
self.name = name
self.objectType = .dagger
self.subItem = Dagger(bladeLength: bladeLength)
}
init(forWeaponAsSword id: Int, name: String, swingFatigue: Int = 3) {
self.id = id
self.name = name
self.objectType = .sword
self.subItem = Sword(swingFatigue: swingFatigue)
}
init(forWeaponAsCrossbow id: Int, name: String, reloadTime: Int = 25) {
self.id = id
self.name = name
self.objectType = .crossbow
self.subItem = Crossbow(reloadTime: reloadTime)
}
}
Now, while the code above works in generation of the objects (and seemingly in encoding the objects), when it comes to decoding it fails to compile as described in the original post.
Again, I can get around this by using multiple sub-classes but this seemed like an elegant solution to the problem, so if there's any way that I can do this, that would be great.
|
|
Here's one way you could do it, using the article Encoding and decoding polymorphic objects in Swift at the blog Digital Flapjack as a guide:
import Foundation
protocol ItemProtocol: Codable {
var itemType: ItemType { get }
var name: String { get set }
var id: Int { get }
}
enum ItemType: String, Codable {
case crossbow, dagger, sword
var metatype: ItemProtocol.Type {
switch self {
case .crossbow: return Crossbow.self
case .dagger: return Dagger.self
case .sword: return Sword.self
}
}
}
struct Dagger: ItemProtocol {
let itemType = ItemType.dagger
let id: Int
var name: String
var bladeLength: Int
}
struct Sword: ItemProtocol {
let itemType = ItemType.sword
let id: Int
var name: String
var swingFatigue: Int
}
struct Crossbow: ItemProtocol {
let itemType = ItemType.crossbow
let id: Int
var name: String
var reloadTime: Int
}
struct ItemWrapper {
var item: ItemProtocol
}
extension ItemWrapper: Codable {
private enum CodingKeys: CodingKey {
case item, itemType
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(ItemType.self, forKey: .itemType)
self.item = try type.metatype.init(from: container.superDecoder(forKey: .item))
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(item.itemType, forKey: .itemType)
try item.encode(to: container.superEncoder(forKey: .item))
}
}
class Armory: Codable {
var weapons: [ItemProtocol]
private enum CodingKeys: CodingKey {
case weapons
}
init(weapons: [ItemProtocol]) {
self.weapons = weapons
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let wrappedItems = try container.decode([ItemWrapper].self, forKey: .weapons)
self.weapons = wrappedItems.map(\.item)
}
func encode(to encoder: Encoder) throws {
let wrappedItems = weapons.map { ItemWrapper(item: $0) }
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(wrappedItems, forKey: .weapons)
}
}
let armory = Armory(weapons: [Dagger(id: 1, name: "Dagger 01", bladeLength: 5),
Sword(id: 2, name: "Sword 01", swingFatigue: 3),
Crossbow(id: 3, name: "Crossbow 01", reloadTime: 25)])
do {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let jsonData = try encoder.encode(armory)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)
if let newArmory = try? JSONDecoder().decode(Armory.self, from: jsonData) {
dump(newArmory)
}
} catch {
print(error)
}
You end up with JSON that is slightly different from what you posted above, but I think it still holds to the spirit of what you're trying to do.
{
"weapons" : [
{
"item" : {
"bladeLength" : 5,
"id" : 1,
"itemType" : "dagger",
"name" : "Dagger 01"
},
"itemType" : "dagger"
},
{
"item" : {
"id" : 2,
"itemType" : "sword",
"name" : "Sword 01",
"swingFatigue" : 3
},
"itemType" : "sword"
},
{
"item" : {
"id" : 3,
"itemType" : "crossbow",
"name" : "Crossbow 01",
"reloadTime" : 25
},
"itemType" : "crossbow"
}
]
}
|
|
Here's another option, to use an array of an enum type that holds each item as its associated value. It's not quite the same thing as conforming a common protocol to Codable , but it's a pretty common way that people do polymorphic Codable conformances.
import Foundation
protocol ItemProtocol {
var id: Int { get }
var name: String { get set }
}
enum Item: Codable {
case crossbow(Crossbow)
case dagger(Dagger)
case sword(Sword)
private enum CodingKeys: CodingKey {
case item, itemType
}
struct InvalidTypeError: Error {
var type: String
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let itemType = try container.decode(String.self, forKey: .itemType)
switch itemType {
case "crossbow":
self = .crossbow(try container.decode(Crossbow.self, forKey: .item))
case "dagger":
self = .dagger(try container.decode(Dagger.self, forKey: .item))
case "sword":
self = .sword(try container.decode(Sword.self, forKey: .item))
default:
throw InvalidTypeError(type: itemType)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .crossbow(let item):
try container.encode("crossbow", forKey: .itemType)
try container.encode(item, forKey: .item)
case .dagger(let item):
try container.encode("dagger", forKey: .itemType)
try container.encode(item, forKey: .item)
case .sword(let item):
try container.encode("sword", forKey: .itemType)
try container.encode(item, forKey: .item)
}
}
}
struct Crossbow: ItemProtocol, Codable {
let id: Int
var name: String
var reloadTime: Int
}
struct Dagger: ItemProtocol, Codable {
let id: Int
var name: String
var bladeLength: Int
}
struct Sword: ItemProtocol, Codable {
let id: Int
var name: String
var swingFatigue: Int
}
class Armory: Codable {
var weapons: [Item]
enum CodingKeys: CodingKey {
case weapons
}
init(weapons: [Item]) {
self.weapons = weapons
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.weapons = try container.decode([Item].self, forKey: .weapons)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(weapons, forKey: .weapons)
}
}
let armory = Armory(weapons: [.dagger(Dagger(id: 1, name: "Dagger 01", bladeLength: 5)),
.sword(Sword(id: 2, name: "Sword 01", swingFatigue: 3)),
.crossbow(Crossbow(id: 3, name: "Crossbow 01", reloadTime: 25))])
do {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let jsonData = try encoder.encode(armory)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)
if let newArmory = try? JSONDecoder().decode(Armory.self, from: jsonData) {
dump(newArmory)
}
} catch {
print(error)
}
(To be honest you probably don't even need ItemProtocol here, but I left it in anyway.)
And this gives us a JSON representation like this, which nicely leaves out the duplication of the itemType information that the first solution had:
{
"weapons" : [
{
"item" : {
"id" : 1,
"name" : "Dagger 01",
"bladeLength" : 5
},
"itemType" : "dagger"
},
{
"item" : {
"id" : 2,
"name" : "Sword 01",
"swingFatigue" : 3
},
"itemType" : "sword"
},
{
"item" : {
"id" : 3,
"name" : "Crossbow 01",
"reloadTime" : 25
},
"itemType" : "crossbow"
}
]
}
There are probably other ways of handling this situation besides the two I posted, but I'll stop here.
|
|
@roosterboy:
Wow, thanks so much for both of those, I'll give them a whirl. I'm not concerned about the actual format of the json, rather the ability to have variable data types embedded without too much of an overhead - your solutions have given me much to work with, thanks x
|
|
Hi All, (and especially @roosterboy)
Here is the solution that I came up with which resolves the issues I was having. Essentially the protocol conforming objects (dagger, etc) now have their data contained in, and extracted from, a string value when saved and loaded, and keeps the generic properties (id, name, etc) at the higher level.
This allows the use of non-concrete properties to be used within Codable conforming objects, and - I think - satisfying my initial requirement for Composition over Inheritance, allowing extensibility of objects without the need for subclassing.
(I'm sure there there'll be concerns about the approach (and I'll be happy to hear them) but I'm just glad to have got this far :) )
Again, huge thanks to @roosterboy!
import Foundation
enum ObjectType: Int, Codable {
case dagger, sword, crossbow
}
protocol ObjectDetailsProtocol: Codable {
func getDetails() -> String
}
class Dagger: ObjectDetailsProtocol {
var bladeLength: Int
required init(bladeLength: Int) {
self.bladeLength = bladeLength
}
init(details: String) {
let jsonData = details.data(using: .utf8)!
let decoder = JSONDecoder()
let info = try! decoder.decode(Dagger.self, from: jsonData)
bladeLength = info.bladeLength
}
func getDetails() -> String {
return "BladeLength: \(bladeLength)"
}
enum CodingKeys: String, CodingKey {
case bladeLength
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(bladeLength, forKey: .bladeLength)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
bladeLength = try container.decode(Int.self, forKey: .bladeLength)
}
}
class Sword: ObjectDetailsProtocol, Codable {
var swingFatigue: Int
required init(swingFatigue: Int) {
self.swingFatigue = swingFatigue
}
init(details: String) {
let jsonData = details.data(using: .utf8)!
let decoder = JSONDecoder()
let info = try! decoder.decode(Sword.self, from: jsonData)
swingFatigue = info.swingFatigue
}
func getDetails() -> String {
return "Swing Fatigue: \(swingFatigue)"
}
enum CodingKeys: String, CodingKey {
case swingFatigue
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
swingFatigue = try container.decode(Int.self, forKey: .swingFatigue)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(swingFatigue, forKey: .swingFatigue)
}
}
class Crossbow: ObjectDetailsProtocol, Codable {
var reloadTime: Int
required init(reloadTime: Int) {
self.reloadTime = reloadTime
}
init(details: String) {
let jsonData = details.data(using: .utf8)!
let decoder = JSONDecoder()
let info = try! decoder.decode(Crossbow.self, from: jsonData)
reloadTime = info.reloadTime
}
func getDetails() -> String {
return "Reload Time \(reloadTime)"
}
enum CodingKeys: String, CodingKey {
case reloadTime
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
reloadTime = try container.decode(Int.self, forKey: .reloadTime)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(reloadTime, forKey: .reloadTime)
}
}
class Object: Codable {
var id: UUID
var name: String
var objectType: ObjectType
var objectDetails: ObjectDetailsProtocol
init(forObjectAsDagger name: String = "Dagger", bladeLength: Int = 5) {
self.id = UUID()
self.name = name
self.objectType = .dagger
self.objectDetails = Dagger(bladeLength: bladeLength)
}
init(forObjectAsSword name: String = "Sword", swingFatigue: Int = 5) {
self.id = UUID()
self.name = name
self.objectType = .sword
self.objectDetails = Sword(swingFatigue: swingFatigue)
}
init(forObjectAsCrossbow name: String = "Crossbow", reloadTime: Int = 5) {
self.id = UUID()
self.name = name
self.objectType = .crossbow
self.objectDetails = Crossbow(reloadTime: reloadTime)
}
func getDetails() {
print("ID: \(id), Name: \(name), Object Type: \(objectType), Object Details: \(objectDetails.getDetails())")
}
enum CodingKeys: String, CodingKey {
case id
case name
case objectType
case objectDetails
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
objectType = try container.decode(ObjectType.self, forKey: .objectType)
let detailsString = try container.decode(String.self, forKey: .objectDetails)
switch objectType {
case .dagger:
objectDetails = Dagger(details: detailsString)
case .sword:
objectDetails = Sword(details: detailsString)
case .crossbow:
objectDetails = Crossbow(details: detailsString)
}
}
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(objectType, forKey: .objectType)
var detailsJSON = ""
switch objectType {
case .dagger:
let dagger = objectDetails as! Dagger
detailsJSON = String(data: try! JSONEncoder().encode(dagger), encoding: .utf8)!
case .sword:
let sword = objectDetails as! Sword
detailsJSON = String(data: try! JSONEncoder().encode(sword), encoding: .utf8)!
case .crossbow:
let crossbow = objectDetails as! Crossbow
detailsJSON = String(data: try! JSONEncoder().encode(crossbow), encoding: .utf8)!
}
try container.encode(detailsJSON, forKey: .objectDetails)
}
}
class Inventory: Codable {
var objects = [Object]()
func addDagger(name: String = "Dagger", bladeLength: Int = 5) {
let dagger = Object(forObjectAsDagger: name, bladeLength: bladeLength)
objects.append(dagger)
}
func addSword(name: String = "Sword", swingFatigue: Int = 5) {
let sword = Object(forObjectAsSword: name, swingFatigue: swingFatigue)
objects.append(sword)
}
func addCrossbow(name: String = "Crossbow", reloadTime: Int = 5) {
let crossbow = Object(forObjectAsCrossbow: name, reloadTime: reloadTime)
objects.append(crossbow)
}
enum CodingKeys: String, CodingKey {
case objects
}
init(){
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
objects = try container.decode([Object].self, forKey: .objects)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(objects, forKey: .objects)
}
}
func main() {
let inventory = Inventory()
inventory.addDagger(bladeLength: 1)
inventory.addDagger(bladeLength: 2)
inventory.addDagger(bladeLength: 4)
inventory.addDagger(bladeLength: 8)
inventory.addDagger(bladeLength: 16)
inventory.addSword()
inventory.addCrossbow()
print("\nInventory:")
for object in inventory.objects {
print(object.getDetails())
}
let encoder = JSONEncoder()
let jsonData = try! encoder.encode(inventory)
let json = String(data: jsonData, encoding: .utf8)!
print("Json = \(json)")
// Clear inventory
inventory.objects.removeAll()
print("\nCleared Inventory:")
for object in inventory.objects {
print(object.getDetails())
}
// Decode JSON back into inventory
let decoder = JSONDecoder()
let decoded = try! decoder.decode(Inventory.self, from: jsonData)
inventory.objects = decoded.objects
// Print decoded inventory
print("\nDecoded Inventory:")
for object in inventory.objects {
print(object.getDetails())
}
}
|