Polymorphic design is a good thing: many design patterns exhibit polymorphism to make the overall system more flexible and extensible.
Unfortunately, Codable
doesn't have "built in" support for polymorphism, at least not yet.... there's also discussion about whether this is actually a feature or a bug.
Fortunately, you can pretty easily create polymorphic objects using an enum
as an intermediate "wrapper."
First, I'd recommend declaring itemType
as a static
property, instead of an instance property, to make switching on it easier later. Thereby, your protocol and polymorphic types would look like this:
import Foundation
public protocol ViewLayoutSectionItemable: Decodable {
static var itemType: String { get }
var id: Int { get }
var title: String { get set }
var imageURL: URL { get set }
}
public struct Foo: ViewLayoutSectionItemable {
// ViewLayoutSectionItemable Properties
public static var itemType: String { return "foo" }
public let id: Int
public var title: String
public var imageURL: URL
// Foo Properties
public var audioURL: URL
}
public struct Bar: ViewLayoutSectionItemable {
// ViewLayoutSectionItemable Properties
public static var itemType: String { return "bar" }
public let id: Int
public var title: String
public var imageURL: URL
// Bar Properties
public var director: String
public var videoURL: URL
}
Next, create an enum for the "wrapper":
public enum ItemableWrapper: Decodable {
// 1. Keys
fileprivate enum Keys: String, CodingKey {
case itemType = "item_type"
case sections
case sectionItems = "section_items"
}
// 2. Cases
case foo(Foo)
case bar(Bar)
// 3. Computed Properties
public var item: ViewLayoutSectionItemable {
switch self {
case .foo(let item): return item
case .bar(let item): return item
}
}
// 4. Static Methods
public static func items(from decoder: Decoder) -> [ViewLayoutSectionItemable] {
guard let container = try? decoder.container(keyedBy: Keys.self),
var sectionItems = try? container.nestedUnkeyedContainer(forKey: .sectionItems) else {
return []
}
var items: [ViewLayoutSectionItemable] = []
while !sectionItems.isAtEnd {
guard let wrapper = try? sectionItems.decode(ItemableWrapper.self) else { continue }
items.append(wrapper.item)
}
return items
}
// 5. Decodable
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Keys.self)
let itemType = try container.decode(String.self, forKey: Keys.itemType)
switch itemType {
case Foo.itemType: self = .foo(try Foo(from: decoder))
case Bar.itemType: self = .bar(try Bar(from: decoder))
default:
throw DecodingError.dataCorruptedError(forKey: .itemType,
in: container,
debugDescription: "Unhandled item type: \(itemType)")
}
}
}
Here's what the above does:
You declare Keys
that are relevant to the response's structure. In your given API, you're interested in sections
and sectionItems
. You also need to know which key represents the type, which you declare here as itemType
.
You then explicitly list every possible case: this violates the Open Closed Principle, but this is "okay" to do as it's acting as a "factory" for creating items....
Essentially, you'll only have this ONCE throughout your entire app, just right here.
You declare a computed property for item
: this way, you can unwrap the underlying ViewLayoutSectionItemable
without needing to care about the actual case
.
This is the heart of the "wrapper" factory: you declare items(from:)
as a static
method that's capable of returning [ViewLayoutSectionItemable]
, which is exactly what you want to do: pass in a Decoder
and get back an array containing polymorphic types! This is the method you'll actually use instead of decoding Foo
, Bar
or any other polymorphic arrays of these types directly.
Lastly, you must make ItemableWrapper
implement the Decodable
method. The trick here is that ItemWrapper
always decodes an ItemWrapper
: thereby, this works how Decodable
is expecting.
As it's an enum
, however, it's allowed to have associated types, which is exactly what you do for each case. Hence, you can indirectly create polymorphic types!
Since you've done all the heavy lifting within ItemWrapper
, it's very easy to now go from a Decoder
to an `[ViewLayoutSectionItemable], which you'd do simply like this:
let decoder = ... // however you created it
let items = ItemableWrapper.items(from: decoder)