網絡請求用 URLSession,JSON 用 Codable。真正嘅 key 要放喺伺服器,唔好放入 iOS、macOS 或 watchOS app。
重點
定義回應模型: 模型保留 rates、source、market_session 同 data_updated_at。金額計算用 Decimal,避免 Double 嘅二進制誤差。
將 key 留喺伺服器: 手機 app 呼叫自己嘅 backend,再由 backend 加 Authorization。伺服器 Swift 可以由環境變數讀 key。
處理狀態同新鮮度: 解碼之前先檢查 HTTP 狀態,分開處理 401 同 429,並保留時間同來源。
定義回應模型
模型保留 rates、source、market_session 同 data_updated_at。金額計算用 Decimal,避免 Double 嘅二進制誤差。
import Foundation
final class NoRedirects: NSObject, URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
completionHandler(nil)
}
}
struct Latest: Decodable {
let base: String
let source: String
let marketSession: String
let dataUpdatedAt: String
let rates: [String: Decimal]
}
struct ExchangeRateClient {
let session: URLSession
let apiKey: String?
func latest(base: String) async throws -> Latest {
let normalizedBase = base.uppercased()
guard normalizedBase.count == 3, normalizedBase.allSatisfy({ $0.isASCII && $0.isLetter }) else {
throw URLError(.badURL)
}
guard var components = URLComponents(string: "https://api.exchangerate.dev/v1/latest/\(normalizedBase)") else {
throw URLError(.badURL)
}
components.queryItems = [URLQueryItem(name: "symbols", value: "EUR,GBP,JPY")]
guard let url = components.url else { throw URLError(.badURL) }
var request = URLRequest(url: url)
request.timeoutInterval = 10
if let apiKey { request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") }
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
throw URLError(.badServerResponse)
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return try decoder.decode(Latest.self, from: data)
}
}將 key 留喺伺服器
手機 app 呼叫自己嘅 backend,再由 backend 加 Authorization。伺服器 Swift 可以由環境變數讀 key。
let key = ProcessInfo.processInfo.environment["EXCHANGERATE_API_KEY"]
let session = URLSession(configuration: .ephemeral, delegate: NoRedirects(), delegateQueue: nil)
let client = ExchangeRateClient(
session: session,
apiKey: key
)
let latest = try await client.latest(base: "USD")
print(latest.rates["EUR"] as Any, latest.source, latest.dataUpdatedAt)處理狀態同新鮮度
解碼之前先檢查 HTTP 狀態,分開處理 401 同 429,並保留時間同來源。
一齊保存匯率上下文
日報可以每日呼叫一次,營運 dashboard 可以每個鐘呼叫一次。收到 429 就降低頻率,唔好即時製造重試風暴。
- 將匯率連同來源同時間保存。
- 用作自動化、估算、報表同分析。
- 唔好當成可以成交嘅報價。
ER
exchangerate.dev
為開發者提供嘅整合指南。
