我正在尝试从API端点返回的数据中呈现视图。我的JSON看起来(大致)是这样的:
{
"sections": [
{
"title": "Featured",
"section_layout_type": "featured_panels",
"section_items": [
{
"item_type": "foo",
"id": 3,
"title": "Bisbee1",
"audio_url": "http://example.com/foo1.mp3",
"feature_image_url" : "http://example.com/feature1.jpg"
},
{
"item_type": "bar",
"id": 4,
"title": "Mortar8",
"video_url": "http://example.com/video.mp4",
"director" : "John Smith",
"feature_image_url" : "http://example.com/feature2.jpg"
}
]
}
]
}
我有一个对象,表示如何在我的UI中布局视图。它看起来像这样:
public struct ViewLayoutSection : Codable {
var title: String = ""
var sectionLayoutType: String
var sectionItems: [ViewLayoutSectionItemable] = []
}
ViewLayoutSectionItemable
是一种协议,其中包括标题和要在布局中使用的图像的URL。
但是,sectionItems
数组实际上由不同类型组成。我想要做的是将每个section项实例化为它自己的类的实例。
如何为init(from decoder: Decoder)
设置ViewLayoutSection
方法让我迭代该JSON数组中的项目并在每种情况下创建正确类的实例?
答案 0 :(得分:4)
多态设计是一件好事:许多设计模式都表现出多态性,使整个系统更加灵活和可扩展。
不幸的是,Codable
没有"内置"支持多态,至少还没有....还讨论了this is actually a feature or a bug。
幸运的是,您可以使用enum
作为中间"包装器轻松创建多态对象。"
首先,我建议将itemType
声明为static
属性,而不是实例属性,以便以后更轻松地启用它。因此,您的协议和多态类型将如下所示:
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 "foo" }
public let id: Int
public var title: String
public var imageURL: URL
// Bar Properties
public var director: String
public var videoURL: URL
}
接下来,为"包装器":
创建一个枚举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)")
}
}
}
以上是做什么的:
您声明与响应结构相关的Keys
。在您的API中,您对sections
和sectionItems
感兴趣。您还需要知道哪个键代表了类型,您在此声明为itemType
。
然后您明确列出每个可能的案例:这违反了Open Closed Principle,但这是"好的"这样做是因为它作为一个"工厂"用于创建项目....
基本上,您只需在整个应用中拥有此 ONCE ,就在这里。
您为item
声明了一个计算属性:这样,您可以打开基础ViewLayoutSectionItemable
而无需需要关心实际的case
这是"包装"的核心。 factory:您将items(from:)
声明为能够返回static
的{{1}}方法,这正是您想要做的:传入[ViewLayoutSectionItemable]
并获取返回一个包含多态类型的数组!这是您实际使用的方法,而不是直接解码Decoder
,Foo
或这些类型的任何其他多态数组。
最后,您必须Bar
实施ItemableWrapper
方法。这里的诀窍是Decodable
始终解码ItemWrapper
:因此,这符合ItemWrapper
期待的方式。
然而,由于它是Decodable
,因此可以使用关联类型,这正是您针对每种情况所做的事情。因此,您可以间接创建多态类型!
由于您已经在enum
内完成了所有繁重的工作,因此 很容易从ItemWrapper
转到`[ViewLayoutSectionItemable ],你只是这样做:
Decoder
答案 1 :(得分:2)
我建议你明智地使用Codable
。如果您只想从JSON解码类型而不对其进行编码,那么单独将其与Decodable
相符就足够了。既然你已经发现你需要手动解码它(通过init(from decoder: Decoder)
的自定义实现),那么问题就变成了:最不痛苦的方法是什么?
首先,数据模型。请注意ViewLayoutSectionItemable
及其采用者不符合Decodable
:
enum ItemType: String, Decodable {
case foo
case bar
}
protocol ViewLayoutSectionItemable {
var id: Int { get }
var itemType: ItemType { get }
var title: String { get set }
var imageURL: URL { get set }
}
struct Foo: ViewLayoutSectionItemable {
let id: Int
let itemType: ItemType
var title: String
var imageURL: URL
// Custom properties of Foo
var audioURL: URL
}
struct Bar: ViewLayoutSectionItemable {
let id: Int
let itemType: ItemType
var title: String
var imageURL: URL
// Custom properties of Bar
var videoURL: URL
var director: String
}
接下来,我们将如何解码JSON:
struct Sections: Decodable {
var sections: [ViewLayoutSection]
}
struct ViewLayoutSection: Decodable {
var title: String = ""
var sectionLayoutType: String
var sectionItems: [ViewLayoutSectionItemable] = []
// This struct use snake_case to match the JSON so we don't have to provide a custom
// CodingKeys enum. And since it's private, outside code will never see it
private struct GenericItem: Decodable {
let id: Int
let item_type: ItemType
var title: String
var feature_image_url: URL
// Custom properties of all possible types. Note that they are all optionals
var audio_url: URL?
var video_url: URL?
var director: String?
}
private enum CodingKeys: String, CodingKey {
case title
case sectionLayoutType = "section_layout_type"
case sectionItems = "section_items"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
title = try container.decode(String.self, forKey: .title)
sectionLayoutType = try container.decode(String.self, forKey: .sectionLayoutType)
sectionItems = try container.decode([GenericItem].self, forKey: .sectionItems).map { item in
switch item.item_type {
case .foo:
// It's OK to force unwrap here because we already
// know what type the item object is
return Foo(id: item.id, itemType: item.item_type, title: item.title, imageURL: item.feature_image_url, audioURL: item.audio_url!)
case .bar:
return Bar(id: item.id, itemType: item.item_type, title: item.title, imageURL: item.feature_image_url, videoURL: item.video_url!, director: item.director!)
}
}
}
用法:
let sections = try JSONDecoder().decode(Sections.self, from: json).sections
答案 2 :(得分:0)
@CodeDifferent响应的简单版本,它处理@ JRG-Developer的注释。无需重新考虑您的JSON API;这是常见的情况。对于您创建的每个新的ViewLayoutSectionItem
,您只需要分别向PartiallyDecodedItem.ItemKind
枚举和PartiallyDecodedItem.init(from:)
方法中添加一个大小写和一行代码即可。
与接受的答案相比,这不仅是最少的代码量,而且性能更高。在@CodeDifferent的选项中,您需要使用2种不同的数据表示形式初始化2个数组,以获得ViewLayoutSectionItem
的数组。在此选项中,您仍然需要初始化2个数组,但是通过利用写时复制语义只能获得一种数据表示形式。
还请注意,协议或采用的结构中不必包含ItemType
(在静态类型化语言中包含描述类型的字符串是没有意义的。)>
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 }
}
}
要处理蛇形案例->驼峰式案例转换,而不是手动键入所有键,只需在JSONDecoder
上设置属性即可
struct Sections: Decodable {
let sections: [ViewLayoutSection]
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let sections = try decode(Sections.self, from: json)
.sections
答案 3 :(得分:0)
我写了一篇关于这个确切问题的blog post。
总之。我建议在 Decoder
extension Decoder {
func decode<ExpectedType>(_ expectedType: ExpectedType.Type) throws -> ExpectedType {
let container = try self.container(keyedBy: PolymorphicMetaContainerKeys.self)
let typeID = try container.decode(String.self, forKey: .itemType)
guard let types = self.userInfo[.polymorphicTypes] as? [Polymorphic.Type] else {
throw PolymorphicCodableError.missingPolymorphicTypes
}
let matchingType = types.first { type in
type.id == typeID
}
guard let matchingType = matchingType else {
throw PolymorphicCodableError.unableToFindPolymorphicType(typeID)
}
let decoded = try matchingType.init(from: self)
guard let decoded = decoded as? ExpectedType else {
throw PolymorphicCodableError.unableToCast(
decoded: decoded,
into: String(describing: ExpectedType.self)
)
}
return decoded
}
}
然后将可能的多态类型添加到 Decoder
实例:
var decoder = JSONDecoder()
decoder.userInfo[.polymorphicTypes] = [
Snake.self,
Dog.self
]
如果您有嵌套的聚合值,您可以编写一个属性包装器来调用此解码方法,这样您就不需要定义自定义 init(from:)
。
答案 4 :(得分:0)
这是一个解决这个确切问题的小 utility package。
它是围绕配置类型构建的,该配置类型具有可解码类型的变体,定义了类型信息 discriminator
。
enum DrinkFamily: String, ClassFamily {
case drink = "drink"
case beer = "beer"
static var discriminator: Discriminator = .type
typealias BaseType = Drink
func getType() -> Drink.Type {
switch self {
case .beer:
return Beer.self
case .drink:
return Drink.self
}
}
}
稍后在您的集合中重载 init 方法以使用我们的 KeyedDecodingContainer
扩展。
class Bar: Decodable {
let drinks: [Drink]
private enum CodingKeys: String, CodingKey {
case drinks
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
drinks = try container.decodeHeterogeneousArray(OfFamily: DrinkFamily.self, forKey: .drinks)
}
}