SwiftUI中使用JSONDecoder解析API数据并在View结构体中展示文本的问题
Let's break down what's going wrong here and fix it step by step—your core issues relate to SwiftUI's state management rules, unhandled JSON decoding edge cases, and improper async operation placement.
Key Problems in Your Current Code
- You can't run async API calls directly in SwiftUI's
body—thebodyis a declarative view description that gets re-evaluated frequently, and async callbacks can't directly update view state. - The Ethermine API returns millisecond timestamps (e.g.,
1699999999000), but yourJSONDecoderdoesn't handle this, so decodingDateproperties will fail silently. - Your API request's error handling is incomplete—you only print "error" instead of passing failure states to the view.
- You're trying to access
currentStatistics.unpaid!without properly referencing the API response instance, and force-unwrapping is risky.
Step 1: Fix Date Decoding for the API Response
First, add a custom JSONDecoder to handle the millisecond timestamps from the API:
extension JSONDecoder { static let ethermineDecoder: JSONDecoder = { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .custom { decoder in let timestamp = try decoder.singleValueContainer().decode(Int64.self) return Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000) } return decoder }() }
Also, update your struct names to follow Swift's UpperCamelCase convention (for consistency):
struct EthermineResponse: Decodable { var status: String var data: DataDetails } struct DataDetails: Decodable { var statistics: [Statistics] var workers: [Workers] var currentStatistics: CurrentStatistics var settings: Settings } // Update all nested structs to UpperCamelCase: Statistics, Workers, CurrentStatistics, Settings
Step 2: Use a ViewModel for State Management
Create an ObservableObject view model to handle API requests and manage state (loading, success, error). This keeps your view code clean and adheres to SwiftUI's state rules:
import Foundation import SwiftUI enum APIState<T> { case idle case loading case success(T) case error(String) } class EthermineViewModel: ObservableObject { @Published var state: APIState<EthermineResponse> = .idle func fetchData(for wallet: String) { state = .loading let request = EthermineRequest(wallet: wallet) request.getData { [weak self] result in DispatchQueue.main.async { // Always update UI state on the main thread switch result { case .success(let data): self?.state = .success(data) case .failure(let error): let errorMessage = error == .noDataAvailable ? "No data received" : "Failed to parse response" self?.state = .error(errorMessage) } } } } } // Fix your API request to handle errors properly struct EthermineRequest { let resourceURL: URL init(wallet: String) { let resourceString = "https://api.ethermine.org/miner/\(wallet)/dashboard" guard let resourceURL = URL(string: resourceString) else { fatalError("Invalid wallet address or API URL") } self.resourceURL = resourceURL } func getData(completionHandler: @escaping (Result<EthermineResponse, DataError>) -> Void) { let task = URLSession.shared.dataTask(with: resourceURL) { data, _, _ in guard let jsonData = data else { completionHandler(.failure(.noDataAvailable)) return } do { let ethData = try JSONDecoder.ethermineDecoder.decode(EthermineResponse.self, from: jsonData) completionHandler(.success(ethData)) } catch { print("Decoding error: \(error)") completionHandler(.failure(.canNotProcessData)) } } task.resume() } } enum DataError: Error { case noDataAvailable case canNotProcessData }
Step 3: Update Your Main View to Display the Data
Now use the view model in your main view to fetch and display the unpaid value, handling all possible states:
struct MainView: View { let wallet = "b5404f020334f52b33012af3587e69305eabee2c" @StateObject private var viewModel = EthermineViewModel() var body: some View { VStack(spacing: 20) { switch viewModel.state { case .idle: Text("Tap to load data") .onTapGesture { viewModel.fetchData(for: wallet) } case .loading: ProgressView("Loading balance...") case .success(let response): Text("Unpaid Balance: \(response.data.currentStatistics.unpaid, specifier: "%.6f") ETH") .font(.title) case .error(let message): Text("Error: \(message)") .foregroundColor(.red) } Spacer() } .padding() .onAppear { viewModel.fetchData(for: wallet) } // Auto-load on view appear } }
Why This Works
- State Management:
@StateObjectand@Publishedensure the view automatically updates when the API state changes. - Main Thread Safety: All state updates happen on the main thread, which is required for SwiftUI view changes.
- Safe Data Handling: We use a switch statement to handle all possible API states (no force-unwrapping!).
- Proper Error Handling: Users get clear feedback if something goes wrong with the API request.
内容的提问来源于stack exchange,提问作者DomTorreto

