SwiftUI中API财务数据无法生成横向BarMark图表求助
SwiftUI 动态生成财务数据横向BarMark图表解决方案
问题核心
原代码存在以下关键问题导致数据集成与图表生成失败:
- 直接使用
KeyPath作为ForEach的id,其哈希与相等性判断不稳定,易引发视图更新异常 Chart初始化语法错误(正确写法为Chart(cashData)而非Chart(data: cashData))- 未给子图表设置固定尺寸,横向滚动时布局混乱
- 缺少数据加载状态与空数据处理,用户体验缺失
修复后的完整代码
import SwiftUI import Charts // 封装财务指标,确保ForEach的id稳定,同时整合KeyPath与显示名称 struct FinancialMetric: Identifiable { let id: UUID = UUID() let keyPath: KeyPath<Cash, Double?> let displayName: String } struct CashChartsView: View { @EnvironmentObject var viewModel: StocksViewModel var stock: StocksResponse // 替换原KeyPath数组为封装后的指标集合 var financialMetrics: [FinancialMetric] = [ FinancialMetric(keyPath: \.inventory, displayName: "Inventory"), FinancialMetric(keyPath: \.netIncome, displayName: "Net Income"), FinancialMetric(keyPath: \.debtRepayment, displayName: "Debt Repayment"), FinancialMetric(keyPath: \.freeCashFlow, displayName: "Free Cash Flow"), FinancialMetric(keyPath: \.dividendsPaid, displayName: "Dividends Paid"), FinancialMetric(keyPath: \.acquisitionsNet, displayName: "Acquisitions (Net)"), FinancialMetric(keyPath: \.netChangeInCash, displayName: "Net Change in Cash"), FinancialMetric(keyPath: \.accountsPayables, displayName: "Accounts Payables"), FinancialMetric(keyPath: \.cashAtEndOfPeriod, displayName: "Cash at End of Period"), FinancialMetric(keyPath: \.commonStockIssued, displayName: "Common Stock Issued"), FinancialMetric(keyPath: \.deferredIncomeTax, displayName: "Deferred Income Tax"), FinancialMetric(keyPath: \.operatingCashFlow, displayName: "Operating Cash Flow"), FinancialMetric(keyPath: \.otherNonCashItems, displayName: "Other Non-Cash Items"), FinancialMetric(keyPath: \.capitalExpenditure, displayName: "Capital Expenditure"), FinancialMetric(keyPath: \.accountsReceivables, displayName: "Accounts Receivables"), FinancialMetric(keyPath: \.otherWorkingCapital, displayName: "Other Working Capital"), FinancialMetric(keyPath: \.changeInWorkingCapital, displayName: "Change in Working Capital"), FinancialMetric(keyPath: \.commonStockRepurchased, displayName: "Common Stock Repurchased"), FinancialMetric(keyPath: \.purchasesOfInvestments, displayName: "Purchases of Investments"), FinancialMetric(keyPath: \.stockBasedCompensation, displayName: "Stock-Based Compensation"), FinancialMetric(keyPath: \.cashAtBeginningOfPeriod, displayName: "Cash at Beginning of Period"), FinancialMetric(keyPath: \.otherFinancingActivites, displayName: "Other Financing Activities"), FinancialMetric(keyPath: \.otherInvestingActivites, displayName: "Other Investing Activities"), FinancialMetric(keyPath: \.effectOfForexChangesOnCash, displayName: "Effect of Forex Changes on Cash"), FinancialMetric(keyPath: \.depreciationAndAmortization, displayName: "Depreciation and Amortization"), FinancialMetric(keyPath: \.salesMaturitiesOfInvestments, displayName: "Sales/Maturities of Investments"), FinancialMetric(keyPath: \.netCashUsedForInvestingActivites, displayName: "Net Cash Used for Investing Activities"), FinancialMetric(keyPath: \.netCashProvidedByOperatingActivities, displayName: "Net Cash Provided by Operating Activities"), FinancialMetric(keyPath: \.investmentsInPropertyPlantAndEquipment, displayName: "Investments in Property/Plant/Equipment"), FinancialMetric(keyPath: \.netCashUsedProvidedByFinancingActivities, displayName: "Net Cash Used Provided by Financing Activities") ] var body: some View { ScrollView(.horizontal) { HStack(spacing: 20) { ForEach(financialMetrics) { metric in VStack(alignment: .center, spacing: 8) { // 指标名称支持换行,居中显示 Text(metric.displayName) .font(.caption) .multilineTextAlignment(.center) // 多状态处理:加载中/有数据/无数据 if viewModel.isLoading { ProgressView() .frame(width: 120, height: 200) } else if let cashData = viewModel.outlook.financialsAnnual?.cash, !cashData.isEmpty { Chart(cashData) { cashEntry in // 同时校验数值与日期的非空,避免无效渲染 if let value = cashEntry[keyPath: metric.keyPath], let date = cashEntry.date { BarMark( x: .value("Period", date), y: .value("Value", value) ) } } .frame(width: 120, height: 200) .chartXAxis { AxisMarks(position: .bottom) { _ in AxisGridLine() AxisTick() AxisLabel(alignment: .center, orientation: .vertical) } } } else { Text("暂无数据") .frame(width: 120, height: 200) .foregroundColor(.gray) } } } } .padding() } .onAppear { Task { await viewModel.getOutlook(symbolID: stock.id) } } } } // 保留原API响应模型(无需修改) // MARK: - OutlookResponse struct OutlookResponse: Codable { let rating: Rating? let ratios: Ratios? let metrics: Metrics? let profile: Profile? let analysis: Analysis? let stockNews: [StockNews]? let insideTrades: [InsideTrade]? let splitHistory: [SplitHistory]? let keyExecutives: [KeyExecutive]? let stockDividend: [StockDividend]? let financialsAnnual, financialsQuarter: Financials? } // MARK: - Financials struct Financials: Codable { let cash: [Cash]? let income: [Income]? let balance: [Balance]? } // MARK: - Cash struct Cash: Codable, Identifiable { var id: UUID = UUID() let date: String? let link: String? let period, symbol: String? let finalLink: String? let inventory, netIncome: Double? let fillingDate, acceptedDate: String? let freeCashFlow, debtRepayment, dividendsPaid, acquisitionsNet: Double? let netChangeInCash, accountsPayables: Double? let reportedCurrency: String? let cashAtEndOfPeriod, commonStockIssued, deferredIncomeTax, operatingCashFlow: Double? let otherNonCashItems, capitalExpenditure, accountsReceivables, otherWorkingCapital: Double? let changeInWorkingCapital, commonStockRepurchased, purchasesOfInvestments, stockBasedCompensation: Double? let cashAtBeginningOfPeriod, otherFinancingActivites, otherInvestingActivites, effectOfForexChangesOnCash: Double? let depreciationAndAmortization, salesMaturitiesOfInvestments, netCashUsedForInvestingActivites, netCashProvidedByOperatingActivities: Double? let investmentsInPropertyPlantAndEquipment, netCashUsedProvidedByFinancingActivities: Double? } // 补充ViewModel加载状态(需在实际ViewModel中实现@Published属性) extension StocksViewModel { var isLoading: Bool { // 实际项目中替换为真实加载状态,例如: // @Published var isLoading = false return false } }
关键改进说明
- 封装指标结构体:用
FinancialMetric整合KeyPath与显示名称,通过UUID提供稳定的id,彻底解决ForEach视图更新异常问题,同时去掉冗余的KeyPath扩展 - 修正Chart语法:使用官方标准的
Chart(dataCollection)初始化方式 - 固定图表尺寸:给每个子图表设置
120x200的固定尺寸,确保横向滚动时布局整齐 - 完善状态处理:添加加载中动画、空数据提示,覆盖全流程用户体验
- 优化轴显示:设置X轴标签垂直排列,避免日期文本重叠
- 严格空值校验:同时校验财务数值与日期的非空性,避免无效数据渲染
内容的提问来源于stack exchange,提问作者user20973290
相关产品推荐
相关产品推荐

