Спасибо за ваш ответ. Я начинаю понимать это. Ваш ответ мне очень помог, и я чувствую, что это должен быть принятый ответ.

аюсь отобразить представление из данных, возвращаемых из конечной точки 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"
        }
      ]
    }    
  ]
}

У меня есть объект, который представляет, как макет представления в моем пользовательском интерфейсе. Это выглядит так:

public struct ViewLayoutSection : Codable {
    var title: String = ""
    var sectionLayoutType: String
    var sectionItems: [ViewLayoutSectionItemable] = []
}

ViewLayoutSectionItemable это протокол, который включает в себя, помимо прочего, заголовок и URL-адрес изображения для использования в макете.

ОднакоsectionItems массив на самом деле состоит из разных типов. То, что я хотел бы сделать, это создать экземпляр каждого элемента раздела как экземпляр своего собственного класса.

Как мне настроитьinit(from decoder: Decoder) метод дляViewLayoutSection позволить мне перебрать элементы в этом массиве JSON и создать экземпляр соответствующего класса в каждом случае?

 Hamish05 окт. 2017 г., 23:48

Ответы на вопрос(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
 Jeff06 окт. 2017 г., 19:06
Один большой вывод для меня из этого состоит в том, что, вероятно, будет лучше переосмыслить то, как я настраиваю и использую мой API, чем пытаться это сделать таким образом.
 Steve Madsen30 июн. 2018 г., 23:18
Будьте осторожны с утверждением: «здесь можно принудительно развернуть, потому что мы уже знаем, какой тип объекта item». Вы знаете, какой тип объекта предметадолжен Да, но если входной JSON искажен и отсутствует одна из этих клавиш, плохие входные данные приведут к падению приложения.
 JRG-Developer15 янв. 2018 г., 14:24
Если ваши типы действительноТОЛЬКО ОграниченFoo а такжеBarПо моему опыту, это вряд ли навсегда останется верным, этот дизайн быстро выйдет из-под контроля. Проблема в этом комментарии:Custom properties of all possible types... что если вам нужно добавитьмного новые типы позже? Или много разных свойств возможно для каждого типа?GenericItem быстро станет огромным ...! Это нехорошо ... По возможности, старайтесь переключать логику на полиморфные объекты и держать фабричные / промежуточные объекты небольшими.

многие шаблоны проектирования демонстрируют полиморфизм, чтобы сделать систему в целом более гибкой и расширяемой.

К несчастью,Codable не имеет "встроенной" поддержки полиморфизма, по крайней мере пока нет ... есть также дискуссия о том,это на самом деле особенность или ошибка.

К счастью, вы можете довольно легко создавать полиморфные объекты, используя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.

Вы тогда явно перечислитевсе возможное случай: это нарушаетОткрытый Закрытый Принцип, но это «нормально», так как действует как «фабрика» для создания предметов ....

По сути, у вас будет только этоОДИН РАЗ во всем приложении, прямо здесь.

Вы объявляете вычисляемое свойство дляitemТаким образом, вы можете развернуть основнойViewLayoutSectionItemable без необходимость заботиться о фактическомcase.

Это сердце фабрики «фантик»: вы заявляетеitems(from:) какstatic метод, который способен вернуть[ViewLayoutSectionItemable], что именно то, что вы хотите сделать: передать вDecoder и получить обратно массив, содержащий полиморфные типы! Этот метод вы будете использовать вместо декодированияFoo, Bar или любые другие полиморфные массивы этих типов напрямую.

Наконец, вы должны сделатьItemableWrapper реализоватьDecodable метод. Хитрость в том, чтоItemWrapper всегда декодируетItemWrapperТаким образом, это работает какDecodable ожидает.

Как этоenumоднако разрешено иметь связанные типы, что в точности соответствует тому, что вы делаете для каждого случая. Следовательно, вы можете косвенно создавать полиморфные типы!

Так как вы сделали всю тяжелую работу внутриItemWrapper, егоочень легко теперь перейти отDecoder в `[ViewLayoutSectionItemable], что вы бы сделали просто так:

let decoder = ... // however you created it
let items = ItemableWrapper.items(from: decoder)
 JRG-Developer05 февр. 2018 г., 21:52
Выне создать экземплярDecoder, Вместо,JSONDecoder создает один внутренне всякий раз, когда вы звонитеtry jsonDecoder.decode(...), Смотрите этот учебник для помощи:raywenderlich.com/172145/...
 invalidArgument06 февр. 2018 г., 16:14
Спасибо за ваш ответ. Я начинаю понимать это. Ваш ответ мне очень помог, и я чувствую, что это должен быть принятый ответ.
 invalidArgument05 февр. 2018 г., 21:24
Я пытаюсь реализовать ваше решение для ответа типа JsonAPI. У меня есть простой вопрос, хотя ... как вы создаете свой декодер ?? JsonDecoder, похоже, не имеет метода, который возвращает один.

Ваш ответ на вопрос