1

I want to decode a dictionary with different values. So while the key will always be of type String, the value will have the same superclass (like Shape) but might be composed out of different subclasses (like Rectangle, Circle). I want to be able to later check which subclass is attached, but so far I can only use the default decoding into [ AttachedObject: Shape ].

See the example:

enum AttachedObject: String, Codable {
    case chair
    case lamp
    case desk
}

class Shape: Codable {
    var name: String

    init(name: String) {
        self.name = name
    }
}

class Rectangle: Shape {
    var width: Double
    var height: Double

    init(name: String, width: Double, height: Double) {
        self.width = width
        self.height = height
        super.init(name: name)
    }

    enum CodingKeys: String, CodingKey {
        case width
        case height
    }

    public override func encode(to encoder: Encoder) throws {
        try super.encode(to: encoder)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.height, forKey: .height)
    }

    required public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        self.width = try values.decode(Double.self, forKey: .width)
        self.height = try values.decode(Double.self, forKey: .height)
        try super.init(from: decoder)
    }
}

class Circle: Shape {
    var radius: Double

    init(name: String, radius: Double) {
        self.radius = radius
        super.init(name: name)
    }

    enum CodingKeys: String, CodingKey {
        case radius
    }

    public override func encode(to encoder: Encoder) throws {
        try super.encode(to: encoder)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.radius, forKey: .radius)
    }

    required public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        self.radius = try values.decode(Double.self, forKey: .radius)
        try super.init(from: decoder)
    }
}

class MyRoom: Codable {
     public var attachedShapes: [ AttachedObject: Shape ]

     enum CodingKeys: String, CodingKey {
         case attachedShapes
     }

     public func encode(to encoder: Encoder) throws {
         var container = encoder.container(keyedBy: CodingKeys.self)
         try container.encode(self.attachedShapes, forKey: .attachedShapes)
     }

     required public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        fatalError("// How to handle the decoding part?")
    }
}
CodingT
  • 13
  • 2

1 Answers1

1

I would go with something like this:

enum ShapeType: String, RawRepresentable, Codable {
    // Required for RawRepresentable
    static var defaultDecoderValue: ShapeType = .circle

    case circle
    case rectangle
}

struct Shape: Codable {
    let name: String
    let width: Double?
    let height: Double?
    let radius: Double?
    let type: ShapeType
}

Then you don't need any custom keys. You can always refer to any of the Shape's in arrays, etc. You can look at ShapeType to see whether it's a rect or circle. You can make them var's instead of lets if you need to change them, and you can make is Class Shape instead of Struct Shape if you want a class instead.

HalR
  • 11,411
  • 5
  • 48
  • 80
  • This does not solve the problem as we just define a an another overarching class and have to sort out afterwards. That way we shift the problem just around. Additionally this does not feel quite right with an increasing amount of classes and properties. I tried to use `.nestedUnkeyedContainer(forKey: .attachedShapes)` so one can decode individually, however that's unkeyed so the key-value-correlation... . – CodingT Mar 27 '20 at 21:29
  • It does take it from 85 lines of code down to 15. You go from 3 classes to one struct. – HalR Mar 27 '20 at 22:33