Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A codable structure contains a protocol property

Tags:

xcode

swift

I have a protocol which is inherited from codable

protocol OrderItem:Codable {
    var amount:Int{get set}
    var isPaid:Bool{get set}
} 

And a struct conform this protocol

struct ProductItem:OrderItem {
    var amount = 0
    var isPaid = false
    var price = 0.0
}

However when I put this structure into a codable structure, I got errors

struct Order:Codable {
    var id:String
    var sn:String = ""
    var items:[OrderItem] = []
    var createdAt:Int64 = 0
    var updatedAt:Int64 = 0
}

The errors are

Type 'Order' does not conform to protocol 'Encodable'
Type 'Order' does not conform to protocol 'Decodable'

But if I change items:[OrderItem] to items:[ProductItem] , everything works!

How can I fix this problem?

like image 642
Tim Avatar asked Jul 30 '17 04:07

Tim


2 Answers

You cannot do that because a protocol only states what you must do. So when you conform your protocol X to Codable, it only means that any type that conforms to X must also conform to Codable but it won't provide the required implementation. You probably got confused because Codable does not require you to implement anything when all your types already are Codable. If Codable asked you to, say, implement a function called myFunction, your OrderItem would then lack the implementation of that function and the compiler would make you add it.

Here is what you can do instead:

struct Order<T: OrderItem>: Codable {
   var id:String
   var sn:String = ""
   var items: [T] = []
   var createdAt:Int64 = 0
   var updatedAt:Int64 = 0
}

You now say that items is a generic type that conforms to OrderItem.

like image 126
Pink Avatar answered Sep 28 '22 07:09

Pink


It's worth mentioning that if you have a property of an array and the type is a protocol: let arrayProtocol: [MyProtocol] and the array contains multiple types that all conform to MyProtocol, you will have to implement your own init(from decoder: Decoder) throws to get the values and func encode(to encoder: Encoder) throws to encode them.

So for example:

protocol MyProtocol {}
struct FirstType: MyProtocol {}
struct SecondType: MyProtocol {}

struct CustomObject: Codable {
   let arrayProtocol: [MyProtocol]

   enum CodingKeys: String, CodingKey {
      case firstTypeKey
      case secondTypeKey
   }
}

so our decode will look like this:

init(from decoder: Decoder) throws {
   let values = try decoder.container(keyedBy: CodingKeys.self)
   // FirstType conforms to MyProtocol
   let firstTypeArray = try values.decode([FirstType].self, forKey: .firstTypeKey)
   // SecondType conforms to MyProtocol
   let secondTypeArray = try values.decode([SecondType].self, forKey: .secondTypeKey)
   // Our array is finally decoded
   self.arrayProtocol: [MyProtocol] = firstTypeArray + secondTypeArray
}

and the same for encoded, we need to cast to the actual type before encoding:

func encode(to encoder: Encoder) throws {
   var container = encoder.container(keyedBy: CodingKeys.self)
   let firstActualTypeArray = arrayProtocol.compactMap{$0 as? FirstType}
   let secondActualTypeArray = arrayProtocol.compactMap{$0 as? SecondType}

   try container.encode(firstActualTypeArray, forKey: .firstTypeKey)
   try container.encode(secondActualTypeArray, forKey: .secondTypeKey)
}
like image 27
OhadM Avatar answered Sep 28 '22 07:09

OhadM