使用Swift Decodable协议解析CoinMarketCap API
我懂你这种遇到动态数字键JSON的头疼——CoinMarketCap这个返回结构确实不怎么友好,硬编码键的方式虽然能跑,但扩展性和可维护性都差点意思。你已经试过David Berry的方案了,这里我分享几个更贴合Swift 4+风格的实现思路,看看能不能适配你的特定场景:
思路1:用动态CodingKey适配所有货币(高扩展性)
如果需要兼容API返回的所有货币,而不是只处理固定几种,这个方法最适合。核心是自定义一个能动态匹配字符串/数字键的CodingKey,再配合分层的Codable模型:
// 自定义动态键,匹配JSON中的数字字符串键 struct DynamicCurrencyKey: CodingKey { var stringValue: String init?(stringValue: String) { self.stringValue = stringValue } var intValue: Int? init?(intValue: Int) { self.stringValue = String(intValue) } } // 货币基础信息模型 struct Currency: Decodable { let id: Int let name: String let symbol: String let maxSupply: Double? // 部分货币max_supply为null,用可选型兼容 let quotes: Quotes enum CodingKeys: String, CodingKey { case id, name, symbol, maxSupply = "max_supply", quotes } } // Quotes容器模型 struct Quotes: Decodable { let usd: USDQuote enum CodingKeys: String, CodingKey { case usd = "USD" } } // USD报价详情模型 struct USDQuote: Decodable { let price: Double let marketCap: Double enum CodingKeys: String, CodingKey { case price, marketCap = "market_cap" } } // 顶层响应模型 struct CoinMarketCapResponse: Decodable { let data: [String: Currency] // 直接用字典接收动态键的货币数据 enum CodingKeys: String, CodingKey { case data } }
解码时的用法非常简洁:
do { let response = try JSONDecoder().decode(CoinMarketCapResponse.self, from: jsonData) // 按ID访问特定货币 if let btc = response.data["1"] { print("比特币价格:\(btc.quotes.usd.price)") } // 转成数组进行排序/过滤 let sortedCurrencies = response.data.values.sorted { $0.id < $1.id } } catch { print("解码失败:\(error.localizedDescription)") }
优点:完全不需要硬编码货币ID,API新增货币时无需修改模型,严格遵循Swift的单一职责原则,模型结构清晰易维护。
思路2:优化硬编码方案(仅处理固定货币)
如果你的业务只需要处理特定几种货币,不想兼容所有返回结果,可以把原来的大结构体拆分成小模型,让解码逻辑更清晰:
// 拆分后的基础模型 struct CurrencyBaseInfo: Decodable { let id: Int let name: String let symbol: String let maxSupply: Double? enum CodingKeys: String, CodingKey { case id, name, symbol, maxSupply = "max_supply" } } struct USDQuote: Decodable { let price: Double let marketCap: Double enum CodingKeys: String, CodingKey { case price, marketCap = "market_cap" } } // 顶层响应模型,仅处理指定货币 struct TargetCurrencyResponse: Decodable { let btc: CurrencyBaseInfo let btcPrice: Double let eth: CurrencyBaseInfo let ethPrice: Double enum RootKeys: String, CodingKey { case data } enum CurrencyIDKeys: String, CodingKey { case btc = "1" case eth = "1027" } enum SubKeys: String, CodingKey { case quotes } init(from decoder: Decoder) throws { let rootContainer = try decoder.container(keyedBy: RootKeys.self) let dataContainer = try rootContainer.nestedContainer(keyedBy: CurrencyIDKeys.self, forKey: .data) // 解析BTC信息 let btcContainer = try dataContainer.nestedContainer(keyedBy: CurrencyBaseInfo.CodingKeys.self, forKey: .btc) btc = try CurrencyBaseInfo(from: btcContainer) let btcQuotesContainer = try dataContainer.nestedContainer(keyedBy: SubKeys.self, forKey: .btc) let btcUSDQuote = try btcQuotesContainer.decode(USDQuote.self, forKey: .quotes) btcPrice = btcUSDQuote.price // 解析ETH信息(逻辑和BTC一致) let ethContainer = try dataContainer.nestedContainer(keyedBy: CurrencyBaseInfo.CodingKeys.self, forKey: .eth) eth = try CurrencyBaseInfo(from: ethContainer) let ethQuotesContainer = try dataContainer.nestedContainer(keyedBy: SubKeys.self, forKey: .eth) let ethUSDQuote = try ethQuotesContainer.decode(USDQuote.self, forKey: .quotes) ethPrice = ethUSDQuote.price } }
优点:把原来混杂的解码逻辑拆分到对应模型,每个模型职责单一,新增/修改目标货币时只需调整枚举和对应解码逻辑,可读性和维护性比你原来的实现好很多。
思路3:遍历动态键做自定义处理(中间态方案)
如果需要处理所有货币,同时还要在解码阶段完成排序、过滤等操作,可以用allKeys遍历动态容器:
struct AllCurrencyResponse: Decodable { let sortedCurrencies: [Currency] // 直接返回排序后的货币数组 enum CodingKeys: String, CodingKey { case data } init(from decoder: Decoder) throws { let rootContainer = try decoder.container(keyedBy: CodingKeys.self) let dataContainer = try rootContainer.nestedContainer(keyedBy: DynamicCurrencyKey.self, forKey: .data) var currencies = [Currency]() // 遍历所有货币键,解码后存入数组 for key in dataContainer.allKeys { let currency = try dataContainer.decode(Currency.self, forKey: key) currencies.append(currency) } // 解码阶段直接按rank排序,后续业务无需再处理 sortedCurrencies = currencies.sorted { $0.id < $1.id } } }
优点:既保留了动态处理所有货币的能力,又能在解码阶段完成数据预处理,减少业务层的逻辑负担。
内容的提问来源于stack exchange,提问作者Edison
相关产品推荐
相关产品推荐

