Swift Charts Won't Update a Variable Value

I am currently working on a project for the Swift Student Challenge. One part of the app is for visualizing goals with a chart created using Swift Charts. In the app, you log your progress for the goal and then it should show up in the chart. My issue is that the data is not showing after it has been logged. Here are some code snippets:

Code For Chart View

Chart {
                let currentDate = Date.now
                
                BarMark (
                    x: .value("Day", "Today"),
                    y: .value("Score", goalItem.getLogItemByDate(date: currentDate).score)
                )
            }
            .frame(maxHeight: 225)
            .padding()

GoalItem Data Object

public class GoalItem: Identifiable {
    public var id: String
    var name: String
    var description: String
    var logItems: [String: GoalLogItem]
    
    init(name: String, description: String) {
        self.id = UUID().uuidString
        self.name = name
        self.description = description
        self.logItems = [:]
    }
    
    func log(date: Date, score: Double, notes: String) {
        self.logItems[dateToDateString(date: date)] = GoalLogItem(date: date, score: score, notes: notes)
    }
    
    func getLogItemByDate(date: Date) -> GoalLogItem {
        let logItem = self.logItems[dateToDateString(date: date)]
        if logItem != nil {
            return logItem!
        } else {
            return GoalLogItem(isPlaceholder: true)
        }
    }
}

After logging something using the GoalItem.log method, why does it not show up in the chart? Are the variables not updated? If so, how would I get the variables to update?

Thanks

Accepted Reply

After reading a few code snippets, I have found the source of this issue. In my chart, I had this line of code: @State var goalItem: GoalItem, it should instead be @StateObject. After making that change, my code is now working normally.

  • A more modern alternative would be to conform your GoalItem class to Observable — this will allow you to keep using @State instead of switching to @StateObject.

Add a Comment

Replies

So ChartView is a SwiftUI view ?

What is the State var or the Observable that is updated when you log something, so that the UI is updated ?

You should provide more code of Chart View.

Also; have a look at this TN: https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro

  • Afaik the app needs to support iOS 16 so the new Observable macro isn't an option unless it's being used as an iOS 17-only feature with an if #available version check or the @available(iOS 17.0, *) attribute.

Add a Comment

I have checked that because I am using an iOS playground I can't use the Observable macro as it seems to be unavailable in an iOS playground. Here is my full code for the SwiftUI view if this helps at all (P.S. I tried adding @State to goalItem but that didn't seem to do anything):

import Charts

enum ChartTimeFrame: String, CaseIterable, Identifiable {
    case week, month, sixMonth, year, all
    var id: Self { self }
}

struct GoalDetailView: View {
    @State var goalItem: GoalItem
    @State private var pickerTimeFrame: ChartTimeFrame = ChartTimeFrame.week
    @State private var showLogSheet: Bool = false
    @State var logScore: Double = 6.0
    @State var logNotes: String = ""
    @State var logDate: Date = Date.now
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        VStack {
            Picker("Timeframe", selection: $pickerTimeFrame) {
                Text("W").tag(ChartTimeFrame.week)
                Text("M").tag(ChartTimeFrame.month)
                Text("6M").tag(ChartTimeFrame.sixMonth)
                Text("Y").tag(ChartTimeFrame.year)
                Text("ALL").tag(ChartTimeFrame.all)
            }
            .pickerStyle(.segmented)
            .padding()
            
            Chart {
                let currentDate = Date.now
                
                BarMark (
                    x: .value("Day", "Today"),
                    y: .value("Score", goalItem.getLogItemByDate(date: currentDate).score)
                )
            }
            .frame(maxHeight: 225)
            .padding()
            
            Spacer()
            
            List {
                NavigationLink(destination: GoalDataList(goalItem: goalItem)) {
                    Text("Show All Data")
                }
                .navigationTitle(goalItem.name)
                .toolbar {
                    Button("Log") {
                        showLogSheet.toggle()
                    }
                    .sheet(isPresented: $showLogSheet) {
                        // goal log sheet code
                        GoalLogSheet(goalItem: goalItem)
                    }
                }
            }
            .scrollDisabled(true)
        }
    }
}

Thanks again

  • I just realized that I forgot to copy the import statement for SwiftUI in my code snippet

  • So, is it solved now ? If so, don't forget to close the thread.

  • The reason why you can't use the new Observable macro is since the Playground must support iOS 16 as well, and Observable requires iOS 17.

Add a Comment

No, sorry for the confusion. The issue isn't fixed now. I was just stating that when I copied my code over into my post, I forgot to copy the import SwiftUI statement so I was clarifying that that wasn't the problem. This may be an issue with iOS Playgrounds but I am unsure. Thanks for your help.

I have done a little bug testing and it seems to be an issue with updating the view. If I add data to the chart via the app and then exit the view and reenter, the information will then show. How would I go about updating the view after new information has been added?

After reading a few code snippets, I have found the source of this issue. In my chart, I had this line of code: @State var goalItem: GoalItem, it should instead be @StateObject. After making that change, my code is now working normally.

  • A more modern alternative would be to conform your GoalItem class to Observable — this will allow you to keep using @State instead of switching to @StateObject.

Add a Comment