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实例,导致数据读取错误。
修改步骤
- 修改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)) } } } }
- 在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) }
- 优化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
相关产品推荐
相关产品推荐

