12

我正在尝试从 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数组实际上是由不同的类型组成的。我想做的是将每个部分项目实例化为其自己的类的实例。

如何设置init(from decoder: Decoder)方法ViewLayoutSection让我遍历该 JSON 数组中的项目并在每种情况下创建正确类的实例?

4

5 回答 5

8

多态设计是一件好事:许多设计模式都表现出多态性,使整个系统更加灵活和可扩展。

不幸的是,Codable没有“内置”对多态性的支持,至少现在还没有……还有关于这实际上是一个特性还是一个错误的讨论。

幸运的是,您可以使用 anenum作为中间“包装器”轻松创建多态对象。

首先,我建议将其声明itemTypestatic属性,而不是实例属性,以便以后更轻松地打开它。因此,您的协议和多态类型将如下所示:

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
}

接下来,为“包装器”创建一个枚举:

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)")
    }
  }
}

以下是上述内容:

  1. 您声明Keys与响应的结构相关。在您给定的 API 中,您对sections和感兴趣sectionItems。您还需要知道哪个键代表您在此处声明为的类型itemType

  2. 然后,您明确列出所有可能的情况:这违反了开放封闭原则,但这是“可以”做的,因为它充当了创建项目的“工厂”......

    从本质上讲,您只会在整个应用程序中拥有此ONCE ,就在此处。

  3. 你为 : 声明一个计算属性,item这样,你可以打开底层ViewLayoutSectionItemable 而不需要关心实际的case.

  4. 这是“包装器”工厂的核心:您声明items(from:)一个static能够返回的方法[ViewLayoutSectionItemable],这正是您想要做的:传入 aDecoder并返回一个包含多态类型的数组!这是您实际使用的方法,而不是直接解码FooBar这些类型的任何其他多态数组。

  5. 最后,您必须ItemableWrapper实现该Decodable方法。这里的诀窍是ItemWrapper 总是解码一个ItemWrapper: 因此,这可以按Decodable预期工作。

但是,因为它是enum,所以它允许有关联的类型,这正是您为每种情况所做的。因此,您可以间接创建多态类型!

由于您已经完成了 内的所有繁重工作ItemWrapper,现在容易从 aDecoder转到 `[ViewLayoutSectionItemable],您只需这样做:

let decoder = ... // however you created it
let items = ItemableWrapper.items(from: decoder)
于 2018-01-15T13:16:42.613 回答
6

@CodeDifferent 响应的更简单版本,用于解决 @JRG-Developer 的评论。无需重新考虑您的 JSON API;这是一种常见的情况。对于你创建的每一个新的,你只需要分别在枚举和方法ViewLayoutSectionItem中添加一个案例和一行代码。PartiallyDecodedItem.ItemKindPartiallyDecodedItem.init(from:)

与公认的答案相比,这不仅是最少的代码量,而且性能更高。在@CodeDifferent 的选项中,您需要使用 2 种不同的数据表示来初始化 2 个数组,以获得您的ViewLayoutSectionItems 数组。在此选项中,您仍然需要初始化 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 }
    }
}

要处理蛇形大小写 -> 驼形大小写转换,而不是手动键入所有键,您可以简单地设置一个属性 onJSONDecoder

struct Sections: Decodable {
    let sections: [ViewLayoutSection]
}

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let sections = try decode(Sections.self, from: json)
    .sections
于 2019-05-27T15:52:49.707 回答
1

我建议您谨慎使用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
于 2017-10-06T03:56:09.503 回答
0

我写了一篇关于这个确切问题的博客文章。

总之。我建议定义一个扩展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
]

如果您有嵌套的聚合值,您可以编写一个属性包装器来调用此 decode 方法,这样您就不需要定义 custom init(from:)

于 2021-04-09T03:37:31.703 回答
0

这是一个解决这个确切问题的小型实用程序包。

它是围绕一种配置类型构建的,该配置类型具有可解码类型的变体,定义了类型信息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)
    }
}
于 2021-05-18T10:40:51.843 回答