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

Swift Charts中ChartView+ForEach在@Published变更时不更新,其余UI正常

问题:Swift Charts图表无法随数据更新

我在Swift Playgrounds中开发一款记录每日情绪并生成图表的小型应用,受环境约束无法使用CoreData,只能通过UserDefaults存储并解析Day数组(仅个人使用,不用于生产环境)。目前遇到的问题是:调用DataManager类的updateData()函数后,大部分UI能正常更新,但Swift Charts的ChartView完全没有反应。

代码实现

Data.swift

import Foundation

struct Day: Hashable, Identifiable, Codable {
    var id: UUID
    var date: Date
    var emotion: Emotion
}

enum Emotion: Hashable, Codable {
    case ExtHappy
    case Happy
    case Good
    case Normal
    case Sad
    case Depressed
    case Angry
}

class DataManager: ObservableObject {
    let storage = UserDefaults.standard
    @Published var data: [Day] = []
    
    init() {
        data = try! JSONDecoder().decode([Day].self, from: Data(storage.string(forKey: "data")?.utf8 ?? "[]".utf8))
    }

    func addEmotion(day: Day) {
        data.append(day)
        updateData()
    }

    func emotionIntToString(value: Int) -> String {
        switch value {
        case 6: return "Amazing"
        case 5: return "Happy"
        case 4: return "Good"
        case 3: return "Normal"
        case 2: return "Sad"
        case 1: return "Depressed"
        case 0: return "Angry"
        default: return "?"
        }
    }
    
    func dayIntToString(val: Int) -> String {
        if val != -1 && val != 0 {
            return "\(-val) days"
        } else if val == -1 {
            return "1 day"
        } else if val == 0 {
            return "Today"
        } else {
            return "?"
        }
    }

    func emotionToInt(emotion: Emotion) -> Int {
        switch emotion {
        case .ExtHappy: return 6
        case .Happy: return 5
        case .Good: return 4
        case .Normal: return 3
        case .Sad: return 2
        case .Depressed: return 1
        case .Angry: return 0
        }
    }

    func emotionToString(emotion: Emotion) -> String {
        switch emotion {
        case .ExtHappy: return "Extrememly Happy"
        case .Happy: return "Happy"
        case .Good: return "Good"
        case .Normal: return "Normal"
        case .Sad: return "Sad"
        case .Depressed: return "Depressed"
        case .Angry: return "Angry"
        }
    }

    func reset() {
        data = []
        updateData()
    }

    func getLatestDay() -> Day {
        if !data.isEmpty {
            var currentIterated = data[0]
            for day in data {
                if currentIterated.date.timeIntervalSince1970 - day.date.timeIntervalSince1970 > 0 {
                    currentIterated = day
                }
            }
            return currentIterated
        } else {
            return Day(id: UUID(), date: Date.now, emotion: .Normal)
        }
    }

    func getNumericValue(day: Day) -> Int {
        let formatter = DateComponentsFormatter()
        formatter.allowedUnits = [.day]
        formatter.unitsStyle = .brief
        return Int((formatter.string(from: day.date.timeIntervalSinceNow)?.replacingOccurrences(of: "day", with: "").replacingOccurrences(of: "days", with: "").replacingOccurrences(of: "s", with: ""))!)!
    }

    private func updateData() {
        objectWillChange.send()
        do {
            let data2 = try JSONEncoder().encode(data)
            if let string = String(data: data2, encoding: .utf8) {
                storage.set(string, forKey: "data")
            }
        } catch {
            print("Error encoding data: \(error.localizedDescription)")
        }
        data = try! JSONDecoder().decode([Day].self, from: Data(storage.string(forKey: "data")?.utf8 ?? "[]".utf8))
        print(data)
    }
}

ChartView.swift

import Charts
import SwiftUI

struct ChartView: View {
    @StateObject var dataManager =  DataManager()
    @State var refresh = false
    
    var body: some View {
        Chart {
            ForEach(dataManager.data) { day in
                if (dataManager.getNumericValue(day: day) >= -5) {
                    LineMark(x:.value("Date", DataManager().getNumericValue(day: day)), y: .value("Emotion", DataManager().emotionToInt(emotion: day.emotion)))
                }
            }
        }.chartXAxis {
            AxisMarks(values: [-5, -4, -3, -2, -1, 0]) { val in
                AxisGridLine(centered: false)
                AxisValueLabel(dataManager.dayIntToString(val: val.as(Int.self) ?? 0))
            }
        }.chartYAxis {
            AxisMarks(values: [0, 1, 2, 3, 4, 5, 6]) { val in
                AxisValueLabel(dataManager.emotionIntToString(value: val.as(Int.self) ?? 0))
            }
        }
    }
}

ContentView.swift

import SwiftUI
import Charts
import Combine

enum ChartOption {
    case bar
    case line
}

struct ContentView: View {
    @StateObject var dataManager = DataManager()
    @State var createSheet = false
    @State var chart: ChartOption = .bar
    @State var refresh = false
    
    var body: some View {
        NavigationStack {
            VStack {
                if (!dataManager.data.isEmpty && !refresh) {
                    TabView {
                        ChartView()
                            .padding(.all, 50).frame(height: 400)
                        ChartViewLine()
                            .padding(.all, 50).frame(height: 400)
                    }.tabViewStyle(.page(indexDisplayMode: .always)).indexViewStyle(.page(backgroundDisplayMode: .always))
                    
                    Button {
                        dataManager.reset()
                    } label: {
                        Text("Reset")
                    }
                } else {
                    Text("Press the + to log your emotion for the day!")
                        .foregroundColor(Color.gray)
                }
            }
            .sheet(isPresented: $createSheet, content: {
                CreateDayModal().presentationDetents([PresentationDetent.medium])
            })
            .navigationTitle("Welcome")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button {
                        createSheet.toggle()
                    } label: {
                        Label("Add", systemImage: "plus")
                    }
                }
            }
        }
    }
}

尝试过的无效方案

  • 尝试通过onChange重新初始化DataManager,没有效果:
.onChange(of: dataManager.data) { val in
    dataManager = DataManager()
}

解决方案

核心问题

ChartView内部通过@StateObject创建了独立的DataManager实例,和ContentView里的DataManager完全是两个对象。当ContentView中的数据更新时,ChartView的实例根本感知不到变化,自然不会更新图表。另外,Chart的LineMark里还错误地创建了新的DataManager实例,导致数据读取错误。

修改步骤

  1. 修改ChartView,接收外部传入的DataManager
    将ChartView中的@StateObject改为@ObservedObject,并添加初始化方法接收外部的DataManager实例:
struct ChartView: View {
    @ObservedObject var dataManager: DataManager
    @State var refresh = false
    
    init(dataManager: DataManager) {
        self.dataManager = dataManager
    }
    
    var body: some View {
        Chart {
            ForEach(dataManager.data) { day in
                if dataManager.getNumericValue(day: day) >= -5 {
                    // 移除新创建的DataManager实例,直接使用当前的dataManager
                    LineMark(x:.value("Date", dataManager.getNumericValue(day: day)), y: .value("Emotion", dataManager.emotionToInt(emotion: day.emotion)))
                }
            }
        }
        .chartXAxis {
            AxisMarks(values: [-5, -4, -3, -2, -1, 0]) { val in
                AxisGridLine(centered: false)
                AxisValueLabel(dataManager.dayIntToString(val: val.as(Int.self) ?? 0))
            }
        }
        .chartYAxis {
            AxisMarks(values: [0, 1, 2, 3, 4, 5, 6]) { val in
                AxisValueLabel(dataManager.emotionIntToString(value: val.as(Int.self) ?? 0))
            }
        }
    }
}
  1. 在ContentView中传递DataManager实例给ChartView
    修改ContentView里的TabView部分,将自身的dataManager传递给ChartView:
TabView {
    ChartView(dataManager: dataManager)
        .padding(.all, 50).frame(height: 400)
    ChartViewLine(dataManager: dataManager) // 同理,ChartViewLine也需要做相同修改
        .padding(.all, 50).frame(height: 400)
}
  1. 优化DataManager的updateData方法
    @Published属性会自动发送变更通知,不需要手动调用objectWillChange.send();另外更新后不需要重新从UserDefaults读取数据,因为data已经是最新状态:
private func updateData() {
    do {
        let data2 = try JSONEncoder().encode(data)
        if let string = String(data: data2, encoding: .utf8) {
            storage.set(string, forKey: "data")
        }
    } catch {
        print("Error encoding data: \(error.localizedDescription)")
    }
    // 移除不必要的重新解码操作
    print(data)
}

完成以上修改后,ChartView就能和其他UI组件一样,随DataManager中的数据变化自动更新图表了。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 12:08:09