Как сделать Swift Codable более универсальным - PullRequest
2 голосов
/ 23 октября 2019

В настоящее время я работаю с API, который имеет дело с предсказаниями шины. С JSON есть интересная особенность, которая возвращается для прогнозов определенной остановки. Когда есть несколько прогнозов для остановки, JSON выглядит примерно так:

...
"direction": {
            "prediction": [
                {
                    "affectedByLayover": "true",
                    "block": "241",
                    "dirTag": "loop",
                    "epochTime": "1571785998536",
                    "isDeparture": "false",
                    "minutes": "20",
                    "seconds": "1208",
                    "tripTag": "121",
                    "vehicle": "1698"
                },
                {
                    "affectedByLayover": "true",
                    "block": "241",
                    "dirTag": "loop",
                    "epochTime": "1571787798536",
                    "isDeparture": "false",
                    "minutes": "50",
                    "seconds": "3008",
                    "tripTag": "122",
                    "vehicle": "1698"
                },
                {
                    "affectedByLayover": "true",
                    "block": "241",
                    "dirTag": "loop",
                    "epochTime": "1571789598536",
                    "isDeparture": "false",
                    "minutes": "80",
                    "seconds": "4808",
                    "tripTag": "123",
                    "vehicle": "1698"
                }
            ],
            "title": "Loop"
        }
...

Однако, когда есть только один прогноз для остановки, JSON выглядит следующим образом:

...
"direction": {
            "prediction": 
                {
                    "affectedByLayover": "true",
                    "block": "241",
                    "dirTag": "loop",
                    "epochTime": "1571785998536",
                    "isDeparture": "false",
                    "minutes": "20",
                    "seconds": "1208",
                    "tripTag": "121",
                    "vehicle": "1698"
                }
            "title": "Loop"
        }
...

Обратите внимание, что «предсказание» больше не находится внутри массива - вот где я считаю, что все усложняется при использовании типа Swift Codable для декодирования JSON. Моя модель выглядит следующим образом для «направления» и «предсказания»

struct BTDirection: Codable {
    let title: String!
    let stopTitle: String!
    let prediction: [BTPrediction]!
}

struct BTPrediction: Codable {
    let minutes: String!
    let vehicle: String!
}

В основном происходит то, что prediction в BTDirection ищет массив BTPrediction, однако во втором случае выше,это не будет массив, поэтому декодирование не удастся. Как я могу сделать свои модели более гибкими для размещения как массива, так и одного объекта? В идеале во втором случае prediction все равно будет массивом из одного BTDirection. Любая помощь по этому вопросу будет высоко ценится.

Ответы [ 3 ]

1 голос
/ 23 октября 2019

Чтобы добавить ответ Sh_Khan, если в ваших ответах API есть несколько мест, где происходят подобные вещи, вы можете извлечь это пользовательское декодирование и кодирование в пользовательский тип оболочки, чтобы вам не приходилось повторять его вездеНапример:

/// Wrapper type that can be encoded/decoded to/from either
/// an array of `Element`s or a single `Element`.
struct ArrayOrSingleItem<Element> {
    private var elements: [Element]
}

extension ArrayOrSingleItem: Decodable where Element: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()

        do {
            // First try decoding the single value as an array of `Element`s.
            elements = try container.decode([Element].self)
        } catch {
            // If decoding as an array of `Element`s didn't work, try decoding
            // the single value as a single `Element`, and store it in an array.
            elements = try [container.decode(Element.self)]
        }
    }
}

extension ArrayOrSingleItem: Encodable where Element: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()

        if elements.count == 1, let element = elements.first {
            // If the wrapped array of `Element`s has exactly one `Element` 
            // in it, encode this as just that one `Element`.
            try container.encode(element)
        } else {
            // Otherwise, encode the wrapped array just as it is - an array
            // of `Element`s.
            try container.encode(elements)
        }
    }
}

// This lets you treat an `ArrayOrSingleItem` like a collection of elements.
// If you need the elements as type `Array<Element>`, just instantiate a new
// `Array` from your `ArrayOrSingleItem` like:
//     let directions: ArrayOrSingleItem<BTDirection> = ...
//     let array: [BTDirection] = Array(directions)
extension ArrayOrSingleItem: MutableCollection {
    subscript(position: Int) -> Element {
        get { elements[position] }
        set { elements[position] = newValue }
    }

    var startIndex: Int { elements.startIndex }
    var endIndex: Int { elements.endIndex }

    func index(after i: Int) -> Int {
        elements.index(after: i)
    }
}

// This lets you instantiate an `ArrayOrSingleItem` from an `Array` literal.
extension ArrayOrSingleItem: ExpressibleByArrayLiteral {
    init(arrayLiteral elements: Element...) {
        self.elements = elements
    }
}

Затем вы можете просто объявить свой prediction (и любое другое свойство, которое может быть либо массивом, либо отдельным элементом в вашем ответе API), например так:

struct BTDirection: Codable {
    let title: String?
    let stopTitle: String?
    let prediction: ArrayOrSingleItem<BTPrediction>?
}
1 голос
/ 23 октября 2019

Можно попробовать

struct BTDirection:Codable {

    let title,stopTitle: String
    let prediction: [BTPrediction]

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        stopTitle = try container.decode(String.self, forKey: .stopTitle)
        do {
            let res = try container.decode([BTPrediction].self, forKey: .prediction)
            prediction = res
        }
        catch { 
              let res = try container.decode(BTPrediction.self, forKey: .prediction)
              prediction = [res] 
        }  
    }
}
0 голосов
/ 23 октября 2019

Оба @TylerTheCompiler & @Sh_Khan обеспечивают очень хороший технический вклад в решение, которое обеспечивает механику решения, но предоставленный код столкнется с некоторыми проблемами реализации с данными json:

  1. естьошибки в JSON, опубликованные, которые прекратят кодируемую работу с ним - я подозреваю, что это просто ошибки копирования и вставки, но если нет, то у вас будут проблемы с продвижением вперед.
  2. Из-за начального direction ключа JSON эффективноимеет 3 (или не менее 2,5!) слоя вложенности. Это либо нужно будет сплющить в init(from:), либо, как показано ниже, понадобится временная структура для простоты отображения. Уплощение в инициализаторе было бы более элегантным, временная структура намного быстрее: -)
  3. CodingKeys, хотя и очевиден, не определен в предыдущих ответах, поэтому вызовет ошибки при компиляции init (from :)
  4. В JSON нет поля stopTitle, поэтому при декодировании возникнет ошибка, если только оно не будет считаться необязательным. Здесь я рассматривал это как конкретный String и обрабатывал его в декодировании;Вы можете просто сделать его String?, и тогда декодер справится с его отсутствием.

Используя «исправленный» JSON (добавленные открывающие скобки, пропущенные запятые и т. д.), следующий код импортирует обасценарии. Я не реализовал arrayOrSingleItem, так как вся заслуга в этом принадлежит @TylerTheCompiler, но вы можете легко вставить его.

struct Direction: Decodable {
   let direction: BTDirection
}

struct BTDirection: Decodable {
   enum CodingKeys: String, CodingKey {
      case title
      case stopTitle
      case prediction
   }
   let prediction: [BTPrediction]
   let title: String
   let stopTitle: String

   init(from decoder: Decoder) throws {
      let container = try decoder.container(keyedBy: CodingKeys.self)
      do {
         prediction = try container.decode([BTPrediction].self, forKey: .prediction)
      } catch {
         let singlePrediction = try container.decode(BTPrediction.self, forKey: .prediction)
         prediction = [singlePrediction]
      }
      title = try container.decode(String.self, forKey: .title)
      stopTitle = try container.decodeIfPresent(String.self, forKey: .stopTitle) ?? "unnamed stop"
   }
}

struct BTPrediction: Decodable {
   let minutes: String
   let vehicle: String
}

, а затем фактически декодировать JSON-декодировать тип Direction верхнего уровня

let data = json.data(using: .utf8)
if let data = data {
   do {
      let bus = try decoder.decode(Direction.self, from: data)
      // extract the BTDirection type from the temporary Direction type
      // and do something with the decoded data
   }catch {
      //handle error
   }
}

Если вы не знаете, JSON Validator очень полезен для проверки / исправления json.

...