I am attempting to render a view from data returned from an API endpoint. My JSON looks (roughly) like this:
{
\"sections\": [
{
\"title\": \"Feature
A simpler version of @CodeDifferent's response, which addresses @JRG-Developer's comment. There is no need to rethink your JSON API; this is a common scenario. For each new ViewLayoutSectionItem
you create, you only need to add one case and one line of code to the PartiallyDecodedItem.ItemKind
enum and PartiallyDecodedItem.init(from:)
method respectively.
This is not only the least amount of code compared to the accepted answer, it is more performant. In @CodeDifferent's option, you are required to initialize 2 arrays with 2 different representations of the data to get your array of ViewLayoutSectionItem
s. In this option, you still need to initialize 2 arrays, but get to only have one representation of the data by taking advantage of copy-on-write semantics.
Also note that it is not necessary to include ItemType
in the protocol or the adopting structs (it doesn't make sense to include a string describing what type a type is in a statically typed language).
protocol ViewLayoutSectionItem {
var id: Int { get }
var title: String { get }
var imageURL: URL { get }
}
struct Foo: ViewLayoutSectionItem {
let id: Int
let title: String
let imageURL: URL
let audioURL: URL
}
struct Bar: ViewLayoutSectionItem {
let id: Int
let title: String
let imageURL: URL
let videoURL: URL
let director: String
}
private struct PartiallyDecodedItem: Decodable {
enum ItemKind: String, Decodable {
case foo, bar
}
let kind: Kind
let item: ViewLayoutSectionItem
private enum DecodingKeys: String, CodingKey {
case kind = "itemType"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DecodingKeys.self)
self.kind = try container.decode(Kind.self, forKey: .kind)
self.item = try {
switch kind {
case .foo: return try Foo(from: decoder)
case .number: return try Bar(from: decoder)
}()
}
}
struct ViewLayoutSection: Decodable {
let title: String
let sectionLayoutType: String
let sectionItems: [ViewLayoutSectionItem]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.title = try container.decode(String.self, forKey: .title)
self.sectionLayoutType = try container.decode(String.self, forKey: .sectionLayoutType)
self.sectionItems = try container.decode([PartiallyDecodedItem].self, forKey: .sectionItems)
.map { $0.item }
}
}
To handle the snake case -> camel case conversion, rather than manually type out all of the keys, you can simply set a property on JSONDecoder
struct Sections: Decodable {
let sections: [ViewLayoutSection]
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let sections = try decode(Sections.self, from: json)
.sections