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

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
    }
}

关键改进说明

  1. 封装指标结构体:用FinancialMetric整合KeyPath与显示名称,通过UUID提供稳定的id,彻底解决ForEach视图更新异常问题,同时去掉冗余的KeyPath扩展
  2. 修正Chart语法:使用官方标准的Chart(dataCollection)初始化方式
  3. 固定图表尺寸:给每个子图表设置120x200的固定尺寸,确保横向滚动时布局整齐
  4. 完善状态处理:添加加载中动画、空数据提示,覆盖全流程用户体验
  5. 优化轴显示:设置X轴标签垂直排列,避免日期文本重叠
  6. 严格空值校验:同时校验财务数值与日期的非空性,避免无效数据渲染

内容的提问来源于stack exchange,提问作者user20973290

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 11:50:54