Swift 中的 JSON 解析(iOS)

Posted

技术标签:

【中文标题】Swift 中的 JSON 解析(iOS)【英文标题】:JSON Parsing in Swift (iOS) 【发布时间】:2020-09-24 02:14:56 【问题描述】:

我想为我的 ios 应用程序解析一些 JSON 数据。我的应用程序此时可以工作,但我觉得如果我决定添加更多货币,我现在拥有的当前解决方案将无法扩展。该应用程序使用选择器视图在多种货币之间进行选择,然后执行 URLSession 数据任务来检索 JSON 数据并对其进行解析。我想知道是否有更好的方法来解析基于不同货币的 JSON 数据。

func parseJSON(_ data: Data, currency: String) -> CurrencyInfo? 
  let decoder = JSONDecoder()
  
  do 
    switch currency 
    case "HKD":
      let decodedData = try decoder.decode(BitcoinDataHKD.self, from: data)
      return CurrencyInfo(price: decodedData.bpi.HKD.rate_float, time: decodedData.time.updatedISO)
    case "GBP":
      let decodedData = try decoder.decode(BitcoinDataGBP.self, from: data)
      return CurrencyInfo(price: decodedData.bpi.GBP.rate_float, time: decodedData.time.updatedISO)
    default:
      fatalError()
    
    
   catch 
    delegate?.didFailWithError(error: error)
    return nil
  


你看,对于每种情况,我都有一个 Decodable 结构。理想情况下,我希望有一个可以处理所有情况的结构,因此在解析 JSON 时不需要使用长的 switch 语句。这是结构的示例。

struct BitcoinDataHKD: Decodable 
  let bpi: BPI
  let time: Time

  struct BPI: Decodable 
    let HKD: HKD

    struct HKD: Decodable 
      let code: String
      let rate_float: Double
    

  
  


struct BitcoinDataGBP: Decodable 
  
  let bpi: BPI
  let time: Time

  struct BPI: Decodable 
    let GBP: GBP

    struct GBP: Decodable 
      let code: String
      let rate_float: Double
    
    
  
  


struct Time: Decodable 
  let updatedISO: String


结构之间唯一不同的是 BPI 结构,可以是“GBP”或“HKD”。

这是来自 CoinDesk 的 JSON 的样子。

https://api.coindesk.com/v1/bpi/currentprice/HKD.json

https://api.coindesk.com/v1/bpi/currentprice/GBP.json

【问题讨论】:

【参考方案1】:

您可以尝试执行以下操作:


struct BitcoinData: Decodable 
  let time: ISOTime
  let bpi: [String: CoinInfo]


struct CoinInfo: Decodable 
  let code: String
  let description: String
  let rate_float: Double


struct ISOTime: Decodable 
  let updatedISO: String



【讨论】:

【参考方案2】:

您可以创建一个模型来解析所有货币并使用[String:Currency] 作为bpi 的类型,而不是创建另一个级别struct/class

struct BitcoinData: Decodable 
    let time: Time
    let bpi: [String:Currency]


struct Time: Decodable 
    let updatedISO: String


struct Currency: Decodable 
    let code: String
    let rateFloat: Double

注意:使用 camel-case 来命名变量。示例:使用rateFloatrate_float

由于 JSON 数据中的键是蛇形大小写的,因此有 2 个选项, 1. 实现enum CodingKeyslike,

struct Currency: Decodable 
    let code: String
    let rateFloat: Double
    
    enum CodingKeys: String, CodingKey 
        case code
        case rateFloat = "rate_float"
    

2.或者你可以在解析时将JSONDecoder'skeyDecodingStrategy设置为convertFromSnakeCase,即

do 
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase //here...
    let response = try decoder.decode(BitcoinData.self, from: data)
    print(response)
 catch 
    print(error)

【讨论】:

【参考方案3】:

bpi 数据视为字典,以便能够使用通用结构。在处理货币汇率时,使用 Decimal 比使用 Double 作为小数部分要好得多,所以我使用汇率的字符串表示形式,并使用 NumberFormatter 将其转换为 Decimal。

struct Root: Decodable 
    let bpi: [String: BitcoinPriceIndex]

我忽略了顶层的其他属性以专注于重要的部分。下面是保存费率的结构,它有一个自定义的初始化,所以我们可以转换为十进制

struct BitcoinPriceIndex: Decodable 
    private static var rateFormatter: NumberFormatter 
        let formatter = NumberFormatter()
        formatter.locale = Locale(identifier: "en_US")
        formatter.numberStyle = .decimal
        return formatter
    

    let currencyCode: String
    let rate: Decimal

    enum CodingKeys: String, CodingKey 
        case currencyCode = "code"
        case rate
    

    init(from decoder: Decoder) throws 
        let container = try decoder.container(keyedBy: CodingKeys.self)
        currencyCode = try container.decode(String.self, forKey: .currencyCode)
        let rateStr = try container.decode(String.self, forKey: .rate)
        rate = BitcoinPriceIndex.rateFormatter.number(from: rateStr)?.decimalValue ?? 0
    

和解码部分

do 
    let result = try JSONDecoder().decode(Root.self, from: data)
    let rates = result.bpi.values
    for rate in rates 
        print(rate.currencyCode, rate.rate)
    
 catch 
    print(error)

10309.7535 美元 7841.9078 英镑


在上述解决方案中,如果格式化程序失败,速率属性将设置为 0,另一种解决方案是在这种情况下回退到双精度值。

将其添加到CodingKeys 枚举(但不是作为属性)

case rate_float

并将init中的解码改为

if let rateFromString = BitcoinPriceIndex.rateFormatter.number(from: rateStr)?.decimalValue 
    rate = rateFromString
 else 
    rate = try container.decode(Decimal.self, forKey: .rate_float)

【讨论】:

以上是关于Swift 中的 JSON 解析(iOS)的主要内容,如果未能解决你的问题,请参考以下文章

iOS swift 3.0 本地 JSON 解析崩溃

Swift 中的特殊 JSON 解析 [关闭]

swift 1.2 中的 JSON 解析问题

如何以正确的方式在 IOS SWIFT 3 中解析 Google 距离矩阵 API JSON

IOS/Swift/JSON:使用 swiftyJSON 解析嵌套的 JSON

将 swift 数组解析为有效的 json