1

Hope you're well! I have an issue where updates to an array in my view model aren't getting picked up until I exit and re-open the app.

Background: My App loads a view from a CSV file hosted on my website. The app will iterate through each line and display each line in a list on the view. Originally I had a function to call the CSV and then pass the data to a String to be parsed each time a refresh was run (user requested or background refresh). This would work for the most part but it did need a user to pull down to refresh or some time to pass for the view to reload (minor issue with the context of the whole app).

I've since changed how the app loads the CSV so it loads it in documentDirectory to resolve issues when theres no internet, the app can still display the data from the last update instead of failing. After running updates to the csv and re-loading it i can see the events variable is getting updated on my view model but not in my list/view. This is a bit of a problem for when the app is first opened as it shows no data as the view has loaded before the csv is parsed. Need to force close the app to have the data load into the list.

I've made some assumptions with the code to share, the csv load & process has no issues as I can print filterviewModel.events before & after the updates and can see changes in the console but not the view. I've also stripped down as much of the shared code so it is easier to read.

Here is the relevant section of my view model:

class EventsListViewModel: Identifiable, ObservableObject {

// Loads CSV from website and processes the data into an structured list.
@Published var events = loadCSV(from: "Eventtest").filter { !dateInPast(value: $0.date) }

}

My View:

struct EventListView: View {
// Calls view model
@ObservedObject var filterviewModel = EventsListViewModel()

var body: some View {

    NavigationView {
        
        // Calls event list from view model and iterates through them in a list.
        List(filterviewModel.events, id: \.id) { event in
            
            //Formats each event in scope and displays in the list.
            eventCell(event: event)

        }
        }
        // Sets the navagation title text.
        .navigationTitle("Upcoming events")
        // When refreshing the view it will re-load the events entries in the view model and refresh the most recent data.
        .refreshable{
            do {
                //This is the function to refresh the data
                pullData()                    
            }
            
        }
        
        // End of the List build
        
    }

    }

Cell formatting (Unsure if this is relevant):

struct eventCell: View {
var event: CSVEvent

@ObservedObject var filterviewModel = EventsListViewModel()

var body: some View {
    
    HStack{
        
        VStack(alignment: .leading, spacing: 5){
            
            //Passes the event location as a link to event website.
            let link = event.url
            Link(event.location, destination: URL(string: link)!)
            
            // Passes the event name to the view.
            Text(event.eventname)
                .font(.subheadline)
                .foregroundColor(.secondary)
            
        }.frame(width: 200.0, alignment: .topLeading)
        
        // Starts new column in the view per event.
        VStack {
            HStack {
                Spacer()
                VStack (alignment: .trailing, spacing: 5){
                    // Passes date
                    Text(event.date)
                        .fontWeight(.semibold)
                        .lineLimit(2)
                        .minimumScaleFactor(0.5)
                    // If time is not temp then display the event start time.
                    Text(actualtime)
                        .frame(alignment: .trailing)
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                }
            }
        }
        
    }
    
}

This is pullData, It retrieves the latest version of the CSV before processing some notifications (notifications section removed for ease of reading, print statement is where i can see the data updating on the view model but not applying)

func pullData(){

@ObservedObject var filterviewModel = EventsListViewModel()

filterviewModel.events = loadCSV(from: "Eventtest").filter { !dateInPast(value: $0.date) }
}

Here is what happens under loadCSV, unsure if this is contributing to the issue as i can see the variable successfully getting updated in pullData

// Function to pass the string above into variables set in the csvevent struct
func loadCSV(from csvName: String) -> [CSVEvent] {
var csvToStruct = [CSVEvent]()

// Create destination URL
let documentsUrl:URL =  (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("testcsv.csv")

//Create string for the source file
let fileURL = URL(string: "https://example.com/testcsv.csv")!

let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)

let request = URLRequest(url:fileURL)

let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
    if let tempLocalUrl = tempLocalUrl, error == nil {
        if let statusCode = (response as? HTTPURLResponse)?.statusCode {
            print("CSV downloaded Successfully")
        }
        
        do {
            try? FileManager.default.removeItem(at: destinationFileUrl)
            try FileManager.default.copyItem(at: tempLocalUrl, to: destinationFileUrl)
        } catch (let writeError) {
            print("Error creating a file \(destinationFileUrl) : \(writeError)")
        }
        
    } else {
        print("Error" )
    }
}
task.resume()

let data = readCSV(inputFile: "testcsv.csv")

//print(data)

// splits the string of events into rows by splitting lines.
var rows = data.components(separatedBy: "\n")

// Removes first row since this is a header for the csv.
rows.removeFirst()

// Iterates through each row and sets values to CSVEvent
for row in rows {
    let csvColumns = row.components(separatedBy: ",")
    let csveventStruct = CSVEvent.init(raw: csvColumns)
    csvToStruct.append(csveventStruct)
}
print("Full file load run")
return csvToStruct
}

func readCSV(inputFile: String) -> String {
//Split file name
let fileExtension = inputFile.fileExtension()
let fileName = inputFile.fileName()

let fileURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let inputFile = fileURL.appendingPathComponent(fileName).appendingPathExtension(fileExtension)

do {
    let savedData = try String(contentsOf: inputFile)
    return savedData
} catch {
    return "Error, something has happened when attempting to retrive the latest file"
}
}

Is there anything obvious that I should be doing to get the list updating when the events array is getting updated in the viewmodel?

Thanks so much for reading this far!

Al McIver
  • 43
  • 1
  • 4
  • You probably only want one data source in memory at once? You probably want your EventsListViewModel to be a data source as an `environmentObject` for this. – TheLivingForce Jun 16 '22 at 23:03

1 Answers1

0

as mentioned, you should have only 1 EventsListViewModel that you pass around the views. Currently you re-create a new EventsListViewModel in your eventCell. Although you don't seem to use it, at least not in the code you are showing us.

The same idea applies to all other views. Similarly for pullData() you should update the filterviewModel with the new data, for example, pass the filterviewModel into it, if it is in another class.

Try this:

EDIT-1: added pullData()

struct EventListView: View {
    // Calls view model
    @StateObject var filterviewModel = EventsListViewModel() // <-- here
    
    var body: some View {
        NavigationView {
            // Calls event list from view model and iterates through them in a list.
            List(filterviewModel.events, id: \.id) { event in
                //Formats each event in scope and displays in the list.
                EventCell(event: event) // <-- here
            }
        }
        .environmentObject(filterviewModel)  // <-- here
        // Sets the navagation title text.
        .navigationTitle("Upcoming events")
        // When refreshing the view it will re-load the events entries in the view model and refresh the most recent data.
        .refreshable{
            do {
                //This is the function to refresh the data
                pullData()
            }
        }
        // End of the List build
    }

    func pullData() {
        filterviewModel.events = loadCSV(from: "Eventtest").filter { !dateInPast(value: $0.date) }
    }

    func loadCSV(from csvName: String) -> [CSVEvent] {
       //...
    }

}

struct EventCell: View {
    var event: CSVEvent
    
    @EnvironmentObject var filterviewModel: EventsListViewModel // <-- here
    
    var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 5){
                //Passes the event location as a link to event website.
                let link = event.url
                Link(event.location, destination: URL(string: link)!)
                // Passes the event name to the view.
                Text(event.eventname)
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }.frame(width: 200.0, alignment: .topLeading)
            // Starts new column in the view per event.
            VStack {
                HStack {
                    Spacer()
                    VStack (alignment: .trailing, spacing: 5){
                        // Passes date
                        Text(event.date)
                            .fontWeight(.semibold)
                            .lineLimit(2)
                            .minimumScaleFactor(0.5)
                        // If time is not temp then display the event start time.
                        Text(actualtime)
                            .frame(alignment: .trailing)
                            .font(.subheadline)
                            .foregroundColor(.secondary)
                    }
                }
            }
        }
    }
}
  • Thanks for this! I do need to brush up my understanding of the different Observed/Environment/State Object's. I've updated the code as above and everything works as expected (no doubt a bit better now too) but theres still no update to the data on view on initial load & redresh as well. App needs to be re-started to reload the view. Could it be something I haven't shared causing it? Also yeah as you expected the EventCell filterviewmodel call's were just edited out to make reading the code a bit more consise. – Al McIver Jun 17 '22 at 07:39
  • you can add `.onAppear { pullData() }` in your `EventListView`, to load your data at the start. Could you show the code for `pullData()`, since it seems to be where the data is not set correctly. – workingdog support Ukraine Jun 17 '22 at 07:49
  • Sorry, I had tried pullData inside .onAppear but looks like I had stripped that out when making the post easier to read. I've updated the original post with the relevant contents of pullData & loadCSV functions. Could it be the ObservedObject under pullData is incorrect? Would tie in with your previous advice! Thanks again, really appreciate it! – Al McIver Jun 17 '22 at 08:27
  • I assume you have `pullData()` in `EventListView`, then just remove `@ObservedObject var filterviewModel = EventsListViewModel()` from it. As I mentioned before, you should have only 1 `EventsListViewModel` – workingdog support Ukraine Jun 17 '22 at 10:34
  • updated my answer with `pullData()` – workingdog support Ukraine Jun 17 '22 at 13:02
  • I've removed the ObservedObject from the PullData function but it didn't seem to make a difference to the issue. I have though been able to fix the issue, albeit maybe a bit hacky/sketchy. What I've done is move the `filterviewModel.events = loadCSV(from: "Eventtest").filter { !dateInPast(value: $0.date) }` into a function inside the viewmodel, and then run the new viewmodel refresh function when the app is first loaded or refreshed. This is now updating the view every time. Its far from best practice I imagine but looks to work well enough! Thank you so much for your help! – Al McIver Jun 17 '22 at 16:17
  • I'm glad you found some way to progress your app. If my answer has help to do that, could you mark it as accepted contribution please. – workingdog support Ukraine Jun 17 '22 at 23:04