Ios 如何使Swift可编码类型更通用

Ios 如何使Swift可编码类型更通用,ios,json,swift,codable,Ios,Json,Swift,Codable,我目前正在使用一个处理总线预测的API。JSON有一个有趣的怪癖,它是为预测某一站而返回的。当有多个停止预测时,JSON如下所示: ... "direction": { "prediction": [ { "affectedByLayover": "true", "block": "241", "dirTag": "loo

我目前正在使用一个处理总线预测的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可编码类型解码JSON时,事情会变得越来越复杂。对于“方向”和“预测”,我的模型是这样的

基本上,正在发生的是
prediction
中的
BTDirection
正在寻找BTPrediction的数组,但是在上面的第二种情况下,这不是数组,因此解码失败。如何使模型更灵活以适应阵列或单个对象?理想情况下,在第二种情况下,
prediction
仍然是单个
BTDirection
的数组。在此方面的任何帮助都将不胜感激。

您可以试试

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] 
        }  
    }
}

为了补充Sh_Khan的答案,如果您的API响应中有多个地方发生了这种情况,您可以将这种自定义解码和编码提取到自定义包装器类型,这样您就不必到处重复,如:

///可以编码/解码到或从中编码/解码的包装类型
///一个'Element'数组或一个'Element'。
结构ArrayOrSingleItem{
私有变量元素:[元素]
}
扩展阵列单项:可解码,其中元素:可解码{
init(来自解码器:解码器)抛出{
let container=尝试解码器。singleValueContainer()
做{
//首先尝试将单个值解码为'Element'数组。
elements=try container.decode([Element].self)
}抓住{
//如果解码为'Element'数组不起作用,请尝试解码
//将单个值作为单个“元素”,并将其存储在数组中。
elements=try[container.decode(Element.self)]
}
}
}
扩展ArrayOrSingleItem:可编码,其中元素:可编码{
func encode(到编码器:编码器)抛出{
var container=encoder.singleValueContainer()
如果elements.count==1,则让element=elements.first{
//如果'Element'的包装数组正好有一个'Element'
//在其中,将其编码为一个“元素”。
尝试container.encode(元素)
}否则{
//否则,将包装好的数组按原样编码-数组
//属于'Element's。
尝试container.encode(元素)
}
}
}
//这使您可以将“ArrayOrSingleItem”视为元素的集合。
//如果需要类型为“Array”的元素,只需实例化一个新的
//您的'ArrayOrSingleItem'中的'Array',如:
//let方向:ArrayOrSingleItem=。。。
//let数组:[BTDirection]=数组(方向)
扩展ArrayOrSingleItem:MutableCollection{
下标(位置:Int)->元素{
获取{elements[position]}
集合{elements[position]=newValue}
}
var startIndex:Int{elements.startIndex}
var endIndex:Int{elements.endIndex}
func索引(在i:Int之后)->Int{
元素索引(在:i之后)
}
}
//这允许您从“Array”文本实例化“ArrayOrSingleItem”。
扩展数组单项:ExpressibleByArrayLiteral{
init(阵列并行元素:元素…){
self.elements=元素
}
}
然后,您可以像下面这样声明您的
预测(以及可能成为API响应中的数组或单个项的任何其他属性):

...
"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"
        }
...
struct BTDirection:Codable{
标题:字符串?
让我们停止标题:字符串?
让预测:ArrayOrSingleItem?
}

无论是@TylerTheCompiler还是@Sh_Khan,都为解决方案提供了非常好的技术输入,提供了解决方案的机制,但是提供的代码会在给定的json数据中遇到一些实现问题:

  • 发布的JSON中有一些错误将停止codable使用它-我怀疑这些只是复制和粘贴错误,但如果不是,您将遇到前进的问题
  • 由于初始的
    方向
    键,JSON实际上有3层(或至少2.5层)嵌套。这要么需要在
    init(from:)
    中展平,要么需要一个临时结构以便于映射。在初始化器中展平会更加优雅,临时结构会更快:-)
  • 编码键虽然很明显,但在前面的回答中没有定义,因此会导致编译init时出错(从:)
  • JSON中没有
    stopTitle
    字段,因此解码时会出错,除非将其视为可选字段。在这里,我将其视为一个具体的
    字符串
    ,并在解码过程中进行处理;你可以把它变成一个
    字符串?
    ,然后解码器就会处理它不存在的问题
  • 使用“更正的”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,解码顶级方向类型

    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非常有用