Swiftui: List not displaying loaded data from viewModel - csv

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!

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

batchUpdate method throws errors while updating Google Slides

I am trying to create a presentation and update it on Google Apps Scripts. The creation is successful. However when I try to update the title or add a new shape or text it throws errors.
Is there any other update method? Also is it possible to update the presentation after modifying the texts without updating all of the presentation? I don't want to create an add-on I just want to be able to update the slides with executing the scripts.
Code:
function createAndUpdatePresentation() {
const createdPresentation = Slides.Presentations.create({"title": "MyNewPresentation"});
const link = `https://docs.google.com/presentation/d/${createdPresentation.presentationId}/edit`;
Logger.log(`Created presentation is on: ${link}`);
const request = {
requests: [
{
updateTitle: {
title: 'My Updated Presentation'
}
}
]
};
const updatedPresentation =
Slides.Presentations.batchUpdate(request, createdPresentation.presentationId);
const updatedLink = `https://docs.google.com/presentation/d/${updatedPresentation.presentationId}/edit`;
Logger.log(`Updated presentation is on: ${updatedLink}`);
}
Error: GoogleJsonResponseException: API call to slides.presentations.batchUpdate failed with error: Invalid JSON payload received. Unknown name "updateTitle" at 'requests[0]': Cannot find field.
Here are two ways to edit a new presentation, one using SlidesApp and the second using Slides API.
function newPresentation1() {
try {
let presentation = Slides.Presentations.create({'title': 'MyNewPresentation'});
presentation = SlidesApp.openById(presentation.presentationId);
let slide = presentation.getSlides()[0];
let element = slide.getPageElements()[0];
element.asShape().getText().setText("Hello")
}
catch(err) {
console.log(err)
}
}
function newPresentation2() {
try {
let presentation = Slides.Presentations.create({'title': 'MyNewPresentation'});
let pageElement = presentation.slides[0].pageElements[0].objectId;
let request = { insertText: { objectId: pageElement,
text: "Good bye" }
};
Slides.Presentations.batchUpdate( { requests: [ request ] }, presentation.presentationId );
}
catch(err) {
console.log(err)
}
}
Reference
SlidesApp
Slides API

Api call to json to interface to Mat-Tree

I'm running into issues with trying to convert a json response from an Api call into an interface that will be accepted by this buildFileTree. So the call is pulling from SQL, it is working in dapper, I also see the array of data in my webapp in my console. The issue is when I try to change the initialize() value for buildFileTree from my static json file 'SampleJson' (inside the project) to my new interface 'VehicleCatalogMod' the tree shows up with SampleJson but when I switch the data to VehicleCatalogMod, the tree collapses.
dataStoreNew: VehicleCatalogMod[] = [];
constructor(private _servicesService: ServicesService){
this._servicesService.GetVehicleCat()
.subscribe(data => {
this.dataStoreNew = [];
this.dataStoreNew = data;
console.log(data);
})
this.initialize();
}
initialize() {
this.treeData = SampleJson;
// Working as SampleJson this is where the problem happens
const data = this.buildFileTree(VehicleCatalogMod, 0);
console.log(data);
this.dataChange.next(data);
}
buildFileTree(obj: object, level: number): TodoItemNode[] {
return Object.keys(obj).reduce<TodoItemNode[]>((accumulator, key) => {
let value = obj[key];
const node = new TodoItemNode();
node.item = key;
if (value != null) {
if (typeof value === 'object') {
node.children = this.buildFileTree(value, level + 1);
} else {
node.item = value;
}
}
return accumulator.concat(node);
}, []);
}
GetVehicleCat(): Observable<any> {
console.log('Vehicle Catalog got called');
return this.http.get('https://api/right/here',
{ headers: this.options.headers });
}
I tried a multitude of different things to try & get this working. I'm pretty much stagnated. Same error occurs when I try this.dataStoreNew instead. No errors in console, it literally just collapses the tree into one non distinguishable line. Also when I used: const vcm = new VehicleCatalogMod(); it made the tree pop up with the different properties but not the API values.
I also attached an image of the HTML element that appears.
with VehicleCatalogMod
with SampleJson

Why is the observable sending old values

I have created a service which sends a request to the backend and makes the result available to the component via an observable.
export class QuestionManagementService {
questionsArray$: Observable<Result>;
private questionsArraySubject: BehaviorSubject<Result>; //to send list of questions
...
constructor(private http: HttpClient, private helper:HelperService, private bs:WebToBackendInterfaceService, private loaderService:LoaderService) {
this.questionsArraySubject = new BehaviorSubject<Result>(new Result('initial',{})); //A Subject can act both as an Observable and an Observer
this.questionsArray$ = this.questionsArraySubject.asObservable(); //create Observable. Other components can subcribe to it now to get notifications/values
...
}
//this method sends the request to network via another `bs` service. The request is sent using `http.post`
getQuestions(questionFilter:GetQuestionsfilter){
console.log("In QuestionManagementService: getQuestions");
let observable:Observable<HttpEvent<any>> = this.bs.getQuestions(questionFilter);
let subscription:Subscription = observable.subscribe((ev:HttpEvent<any>)=>{
if(ev.type === HttpEventType.Response) { //null means that the response wasn't an HttpResponse but probably some internal Rxjs event (eg type 0)
let response= <HttpResponse<any>>ev;
console.log("http response received: ",response);
//should remove the token from storage
console.log('response body from server: ',ev.body);
let isResponseStructureOK: boolean = this.helper.validateServerResponseStructure(ev.body);
if (isResponseStructureOK) {
console.log("response structure is OK");
let response: ServerResponseAPI = ev.body;
let result:string = response.result;
console.log("result is : " + result);
/*if result could be success or error*/
/*additionalInformation is a string and the string contains a valid json which has array of questions
in format {"questions-list":[{"tag":"some tag1","description":"some description1"},{{"tag":"some tag2","description":"some description2"},...]}
*/
let message:string = response['additional-info'];
console.log("message is "+message);
if(result === "success") {
let jsonQuestionList: string = response['additional-info'];
console.log("jsonQuestionList response as string: ", jsonQuestionList);
//let jsonQuestions: PracticeQuestionsListAPI = JSON.parse(jsonQuestionList);
//console.log("jsonQuestion array:", jsonQuestions);
//this.questionsArraySubject.next(jsonQuestions['questions-list']);
this.questionsArraySubject.next(new Result('success', response["additional-info"]));
} else {
this.questionsArraySubject.next(new Result('error', response["additional-info"]));
}
}
else {
/**
* If something goes wrong, send error rather than send next with result="error"
*/
console.log("received incorrect response structure from server: ", ev.body);
//TODOM - need to change hard coded responses and pick them from a config or global variable.
this.questionsArraySubject.error(new Result('error',"Invalid response structure from server"));
}
}
else {
console.log("not response. ignoring");
}
},
(error:ServerResponseAPI)=>{/*web to backend service will send error in ServerResponseAPI format. This is what handleError throws*/
console.log("got error from the Observable: ",error);
this.questionsArraySubject.error(new Result('error',error['additional-info']));
},
()=>{ //observable complete
console.log("observable completed")
});
}
}
The following component subscribes to this service.
export class PraticeQuestionListComponent implements OnInit, OnDestroy {
questions: PracticeQuestionsListAPI; //the result from observable will be stored here.
questionListSubscription:Subscription; //reference of the subscription
ngOnDestroy(): void {
console.log("destroying component. unsubscribing");
this.questionListSubscription.unsubscribe()
}
//on initialisation, I subscribe to the observable
ngOnInit(){
console.log("in question list on init. question is ",this.questions);
...
this.questions= new PracticeQuestionsListAPI(new AdditionalPagingInfo("",new PartitionInfo(0,0)),
[]);
let tagSubscription = this.questionManagementService.getSupportedTags(new TagId("coding"));
console.log("subscribing to question mgmt service");
this.questionListSubscription = this.questionManagementService.questionsArray$.subscribe((result:Result)=>{
console.log('received result from question mgmgt service - array observable',result);
if(result.result === "success") { //received response from server
let questionList = JSON.parse(result.additionalInfo) as PracticeQuestionsListAPI;
console.log("got list of questions value ", questionList);
this.questions['pagination-info'] = questionList['pagination-info'];
this.questions['questions-list'] = questionList['questions-list'];
/*
0 length of questions-list means no questions.
this could be response from the server indicating that there are no more questions
*/
/*
* the server indicates that there are no more questions by either sending empty question list or by sending
* 0 values for pagination state and partition info
*/
if (questionList["questions-list"].length !== 0) { //server has send list of questions
this.questions['pagination-info']['page-state'] = questionList['pagination-info']['page-state'];
this.questions['pagination-info']['partition-info'] = questionList['pagination-info']['partition-info'];
this.questions['questions-list'] = questionList['questions-list'];
console.log("previous question filter is ",this.questionsFilter);
this.questionsFilter["pagination-info"]["page-state"]=questionList["pagination-info"]["page-state"];
this.questionsFilter["pagination-info"]["partition-info"].month=questionList["pagination-info"]["partition-info"].month;
this.questionsFilter["pagination-info"]["partition-info"].year=questionList["pagination-info"]["partition-info"].year;
console.log("new question filter is ",this.questionsFilter);
//TODOM - maybe this assignment below was causing memory leak. So changed this as above
//this.questionsFilter['pagination-info'] = questionList['pagination-info'];
this.lastPage = false; //the server indicates that there are no more questions by sending these values (no paging state and no partition info)
if (this.questions['pagination-info']['page-state'].length == 0 &&
this.questions['pagination-info']['partition-info'].year == 0 &&
this.questions['pagination-info']['partition-info'].month == 0) {
this.lastPage = true;
} else {//if the list is empty then there are no (more) questions for the selected tag
this.lastPage = false;
}
} else {
this.lastPage = true; //Don't show next button if there are no questions.
this.showDialog(new PracticeQuestionListContext("Reached end of the search. No more results available", new PracticeQuestionListAdditionalInfo()));
}
} else {
//TODOM - I should probably display the error in case there is an error from the server
console.log("ignoring value");
}
},
(err:Result)=>{
console.log("received error from QuestionArray observable",err);
//TODOM - probably should change the name of DialogContext to Component specific additional context
this.showDialog(new PracticeQuestionListContext(err.additionalInfo,new PracticeQuestionListAdditionalInfo()));
},
()=>{
console.log("question mgmt service, questionarray observable completed.");
});
}
}
The issue I am facing is that if I visit the component for the first time, I get the values from the observable after making some selections in the UI (expected behavior). Then i visit the home page of the application, the component gets destroyed (again, expected behavior). Then if I visit the component again, the observable sends the old values (from the first visit) even when I have not made any UI selections.
Why is the observable sending old values and how can I stop it from doing that? I have created another question in SO with pictures which might explain the scenario better
angular component is retaining old value maybe because the observable is resending past data

How to get Spotify playlist tracks and parse the JSON?

I am trying to figure out how to parse the JSON data that I am getting from the Spotify API. I am using this node module https://www.npmjs.com/package/spotify-web-api-js to get Spotify playlist tracks.
I am using this to GET my json (see what I did there)
export class HomePage {
spotifyApi = new SpotifyWebApi;
constructor() {}
}
var spotifyApi = new SpotifyWebApi();
spotifyApi.setAccessToken('Spotify OAuth Token');
spotifyApi.getPlaylistTracks('37i9dQZEVXbMDoHDwVN2tF')
.then(function(data) {
console.log('Playlist Tracks', data);
}, function(err) {
console.error(err);
var prev = null;
function onUserInput(queryTerm) {
// abort previous request, if any
if (prev !== null) {
prev.abort();
}
// store the current promise in case we need to abort it
prev = spotifyApi.searchTracks(queryTerm, {limit: 5});
prev.then(function(data) {
// clean the promise so it doesn't call abort
prev = null;
// ...render list of search results...
}, function(err) {
console.error(err);
});
}
This returns a JSON file but for some reason (probably my mistake) when I use JSON.parse(data);
console.log(data.name) it doesn't work (I know I am doing something wrong here but I don't know how to fix it). Thanks in advance :{)
If you want to get the tracks from the url you have to do this data.tracks.track[0] replace 0 with the needed tracks.

Refresh UIViewController with new Data in Swift

I'm building an exercise app. I have an array of exercises in my TableViewController, with each cell displaying a different exercise. The cells segue to a UIViewController. Within the UIViewController I would now like a user to be able to skip to the next exercise without having to go back to the TableViewController.
How can I refresh my ViewController containing the new data of the next exercise? (Similar to the reloadData method when constructing tables?)
I'm getting the next exercise in the array, but my page isn't refreshing.
My code:
var exercise: Exercise?
var exerciseList: [Exercise]!
// toolbar item button pressed:
#IBAction func nextExercise(sender: UIBarButtonItem) {
if let currentIndex = exerciseSet.indexOf(exercise!) {
let nextIndex = currentIndex + 1
let nextExercise = exerciseList[nextIndex]
reloadData(nextExercise)
}
}
private func reloadData(displayedExercise: Exercise){
exercise = displayedExercise
self.view.setNeedsDisplay()
}
Thanks!
You can use our codes and easily can do pagination. We already answered your question.
Example for load more indexs;
if indexPath.row == privateList.count - 1 { // last cell
if totalItems > privateList.count { // more items to fetch
loadItem() // increment `fromIndex` by 20 before server call
}
}
Swift tableView Pagination
Thanks