Refresh UIViewController with new Data in Swift - uiviewcontroller

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

Related

Swiftui: List not displaying loaded data from viewModel

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

How to limit function to once in every x SWIFT

so I have made a comments option on my app which lets the users comment something on posts. It is directly referenced and sends info from the string to the api endpoint. That works, now I was wondering if there was a way to limit the number of essentially requests to the API so users can send comments once every minute per say
TextField("Comment...", text: $comment) { editingChanged in
} onCommit: {
validate()
showsAlert = !isValid
if isValid{
viewModel.sendComment(nickname: nickname, body: comment) {
self.comment = ""
}
}
}
}
.padding()
.frame(width: UIScreen.main.bounds.width, height: 80)
.ignoresSafeArea(.keyboard, edges: .bottom)
It would help me reduce spam on the comments and would be a life saver
You can create a simple Swift class to control the time.
public class Time_Control:Thread{
var wait_time:Int //In seconds
public var can_send:Bool = true
init(_ wait_time:Int) {
self.wait_time = wait_time
}
public override func start() {
super.start()
self.can_send = false
Thread.self.sleep(forTimeInterval: TimeInterval(self.wait_time))
self.can_send = true
}
}
Then you can use this object in any part of your code to control time between events. For this, first have a variable to store the object of the class Time_Control, for example, var control:Time_Control = Time_Control(0). After that, each time you want to control the time of something just do the following:
func send_message(){
if control.can_send{
//Start the time controller with 60 seconds
control = Time_Control(60)
control.start()
//Let the user send message
//... your code
}else{
//Don't let the user send message
//...
}
}
When the user interacts with your UI, you can call the function send_message(). It will allow the user to send a message every 60 seconds.
In Swift you can get the Unix timestamp of the current moment using:
let currentUnixTimestamp = Date().timeIntervalSince1970
Unix time simply counts the seconds that have elapsed since 1 Jan, 1970 00:00:00 UTC. See Wikipedia to learn more.
Using this, you could create a field in which you save the current timestamp every time a comment is sent and then check the current time against the saved timestamp. If your viewModel is of a class type, which I assume it is, it is the perfect place to put this code. It might look like this:
class ViewModel {
// How many seconds to wait before the user can comment again
private static let cooldownInSeconds = 60
private var lastSendTimestamp: Int? = nil
func sendComment(nickname: String, body: String) {
let currentTime = Date().timeIntervalSince1970
if currentTime >= (lastSendTimestamp ?? Int.min) + ViewModel.cooldownInSeconds {
lastSendTimestamp = currentTime
// Send comment here
} else {
// Cannot send comment because cooldown has not run out
}
}
}
Note that putting the variable in structs (like your View) will not work well, due to their immutable nature.

Why does it only returns 11 of my loop as I scrape using puppeteer? Click "show more" button until reaches the end

I have this code:
with a url of: block.hacash.org
I am using Puppeteer on Firefox, headless, hosted on VPS.
function hacdName() {
const extractedElements = document.querySelectorAll('td.t2.gray.dts');
const items = [];
for (let element of extractedElements) {
items.push(element.innerText);
}
return items;
}
let items = await page.evaluate(hacdName);
console.log(items)
However the output will only return the first 10 (ten) td.t2.gray.dts and will not return the rest.
Example:
[
'00000000097f5ff183be...',
'0000000019bcba6a3eae...',
'0000000014895c593f29...',
'00000000077088d50229...',
'0000000001a143b70894...',
'0000000013c9089db9cb...',
'0000000006b7a707923c...',
'000000001bfa7c9c68ec...',
'00000000030593fa3c73...',
'000000001af596b772c5...',
'000000000394daca889b...'
]
How can I scrape all td.t2.gray.dts to block 0 or until I consumed all show more button.
Another question :
Why does puppeteer on td.tr.gray.dts return (e.g. 000000000394daca889b...) not the whole (000000000394daca889b9f472f6ede90a9e835bc516d7f76f37718e5d827e6b2)?
In order to get all elements using the Puppeteer you need to have them on page first, to do this you can create a function that will click on the "Show More" button until it's visible
async function loadAllResources(page) {
let moreButton;
do {
moreButton = await page.$('#blocks .pmore .button');
if (moreButton) {
await moreButton.click();
await page.waitFor(1000); // some wait until the rows will be loaded
}
} while(moreButton)
}
and just call it before scraping all data await loadAllResources(page);
As for the question with no full text, it's because you're getting the innerHtml. To obtain the whole text you need to get the title from the link
function hacdName() {
const extractedElements = document.querySelectorAll('td.t2.gray.dts a');
const items = [];
for (let element of extractedElements) {
items.push(element.title);
}
return items;
}
Also, I will suggest you pay attention to the API and monitor the network tab when you're loading this resource, for example, fetching the data from the URL http://block.hacash.org/api/block/list?last=170246&limit=1000 will be much easier and faster and can solve the same task faster.

How to show data in QML json request

So bear with me. How to create a model based on json? What is delegate?
Is below logic is correct?
Model -> delegate -> json request -> json get -> show to list view
In below code I can not see any data on screen. How to show data in QML json request?
thanks
UPDATED WORKING CODE:
import VPlayApps 1.0
import QtQuick 2.0
import QtQuick 2.3
import QtQuick.Controls 1.2
import "qrc:/"
Item {
id: item1
anchors.fill: parent
ListModel {
id: ***modelListIP***
}
ListView {
id: listview
anchors.fill: parent
model: ***modelListIP***
delegate: Text {
text: listdata
}
}
function getData() {
var xmlhttp = new XMLHttpRequest();
var url = "https://api.ipify.org?format=json";
xmlhttp.onreadystatechange=function() {
if (xmlhttp.readyState == XMLHttpRequest.DONE && xmlhttp.status == 200) {
myFunction(xmlhttp.responseText);
}
}
xmlhttp.open("GET", url, true);
xmlhttp.send();
}
function myFunction(response) {
var objValue = JSON.parse(response);
***modelListIP.append( {"listdata": objValue.ip })***
}
Button {
anchors.bottom: parent.bottom
width: parent.width
text: "Get Data"
onClicked: getData()
}
}
This tested on Qt5.9.2 using QML app project.
Your example is totally wrong.
JSON.parse() returns Object, not array. So you cannot call length() on it. Remember - {} - object, [] - array.
Your request returns something like {"ip":"111.111.111.111"}. Where do you see Name here? So you should append items model.append( {"listdata": arr.ip }), not like you do it now. Don't forget to surround the parameter name with quotes.
listview.model.append shoud be replaced with model.append. Learn what is Occam's razor.
model is not good id for item. Using reserved words is a bad style.
So I advice you to read documentation twice when you facing such problems.

In function call, why parse statement get executed atlast in SWIFT?

I am begining to SWIFT. I have tried following code to check how function call is working. Function call is working fine, as our wish. But, in PARSE, the order is not working in the parse statement. Parse statement get executed atlas when all function gets over. How to solve this.
if I run this code, I am getting output like,
MY OUTPUT:
START
FIRST CLASS TOP
FIRST CLASS BOTTOM
SECOND CLASS
THIRD CLASS
END
WELCOME TO PARSE // WHY THIS LINE IS PRINTING LAST??
But, I need output like,
REQUIRED OUTPUT:
START
FIRST CLASS TOP
WELCOME TO PARSE //I NEED THIS LINE TO BE EXECUTE IN ORDER.
FIRST CLASS BOTTOM
SECOND CLASS
THIRD CLASS
END
MY CODING IS BELOW. Kindly check and guide me.
class ViewController: UIViewController {
let one_1 = class_1()
let second_2 = class_2()
let third_3 = class_3()
override func viewDidLoad() {
super.viewDidLoad()
println("START")
one_1.one()
second_2.second()
third_3.third()
println("END")
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
//CLASS_1
class class_1 {
var par_query = PFQuery(className: "story")
func one() {
println("FIRST CLASS TOP")
par_query.findObjectsInBackgroundWithBlock({(NSArray objects, NSError error) in
if (error != nil) {
NSLog("error " + error.localizedDescription)
}
else {
println("WELCOME TO PARSE")
}//ELSE ENDING
}) //PARSE ENDING
println("FIRST CLASS BOTTOM")
}
}
//CLASS_2
class class_2 {
func second() {
println("SECOND CLASS")
}
}
//CLASS_3
class class_3 {
func third() {
println("THIRD CLASS")
}
}
It has nothing to do with parse.com in particular, it behaves that way because findObjectsInBackgroundWithBlock is executed asynchronously.
You can read about that here.
UPDATED:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
//Do some time comsuming calculation
//And when it's done
dispatch_async(dispatch_get_main_queue()) {
//Update the UI with your results
}
}
UPDATED 2
Let me put it that way: if you print anything, like "WELCOME TO PARSE" in an asynch closure, you cannot determine when it will be executed. I will mark with an * that where can be the "WELCOME TO PARSE" message with your current code:
START
FIRST CLASS TOP
FIRST CLASS BOTTOM
*
SECOND CLASS
*
THIRD CLASS
*
END
*
If you want to print the exact lines as you want, you could do two things:
Do not put a println in the asynch block
Put "FIRST CLASS BOTTOM" in your asynch block, and put also
class_2().second()
class_3().third()
in the asych block, so that will be called after the block is executed. However, I do not recommend this, it's just for the example.