You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 07:13:34