Home > Software design >  Swiftui: List not displaying loaded data from viewModel
Swiftui: List not displaying loaded data from viewModel

Time:06-22

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!

CodePudding user response:

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)
                    }
                }
            }
        }
    }
}
  • Related