Pull to Refresh: data refresh is delayed - ios

I've got Pull to Refresh working great, except when the table reloads there is a split second delay before the data in the table reloads.
Do I just have some small thing out of place? Any ideas?
viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl?.addTarget(self, action: "handleRefresh:", forControlEvents: UIControlEvents.ValueChanged)
self.getCloudKit()
}
handleRefresh for Pull to Refresh:
func handleRefresh(refreshControl: UIRefreshControl) {
self.objects.removeAll()
self.getCloudKit()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
refreshControl.endRefreshing()
})
}
Need the data in two places, so created a function for it getCloudKit:
func getCloudKit() {
publicData.performQuery(query, inZoneWithID: nil) { results, error in
if error == nil { // There is no error
for play in results! {
let newPlay = Play()
newPlay.color = play["Color"] as! String
self.objects.append(newPlay)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
} else {
print(error)
}
}
}
tableView:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)
let object = objects[indexPath.row]
if let label = cell.textLabel{
label.text = object.matchup
}
return cell
}

This is how you should do this:
In your handleRefresh function, add a bool to track the refresh operation in process - say isLoading.
In your getCloudKit function just before reloading the table view call endRefreshing function if isLoading was true.
Reset isLoading to false.
Importantly - Do not remove your model data before refresh operation is even instantiated. What if there is error in fetching the data? Delete it only after you get response back in getCloudKit function.
Also, as a side note, if I would you, I would implement a timestamp based approach where I would pass my last service data timestamp (time at which last update was taken from server) to server and server side would return me complete data only there were changes post that timestamp else I would expect them to tell me no change. In such a case I would simple call endRefreshing function and would not reload data on table. Trust me - this saves a lot and gives a good end user experience as most of time there is no change in data!

Related

How to ensure the order of list shown in UITableView when getting data for cell from UIDocument

My app fetches data via FileManager for each cell in UITableView. The cells need data from a file which requires open UIDocument objects. However, it seems like code inside open completion handler get executed non predictably, so the cells don't get displayed in the order I wish.
How do I solve this problem? I appreciate if anyone gives any clue.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
fetchCellsData()
}
func fetchCellsData() {
do {
let files = getUrlsOfFiles() //files in a certain order
for file in files {
let document = MyDocument(fileURL: file) //MyDocument subclassing UIDocument
//open to get data for cell
document.open { (success) in
if !success {
print("File not open at %#", file)
return
}
let data = populateCellData(document.fileInfo)
self.cellsData.append(data)
self.tableView.reloadData()
/// tried below also
// DispatchQueue.main.async {
// self.cellsData.append(data)
// self.tableView.reloadData()
// }
}
document.close(completionHandler: nil)
}
} catch {
print(error)
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! MyCell
let fileInfo = cellsData[indexPath.row].fileInfo
//assign fileInfo into MyCell property
return cell
}
I expect cells get rendered in the order of 'files', however, it seems like the order is a bit unpredictable presuming that it's due to the fact that 'cellForRowAt' gets called before it knows about the full set of 'cellsData'.
From Apple documentation on UIdocument . Apple doc:
open(completionHandler:)
Opens a document asynchronously.
Which means that even if you trigger document.open in the right order, nothing guarantees that the completionHandler sequence will be in the same order, this is why the order is unpredictible.
However, you know that they will eventually all get done.
What you could do is :
1 - place all your datas from opened document into another list
2 - order this list in accordance to your need
3 - place this list into cellsData (which I assume is bound to your tableViesDatasource)
var allDatas: [/*your data type*/] = []
...
do {
// reset allDatas
allDatas = []
let files = getUrlsOfFiles()
for file in files {
let document = MyDocument(fileURL: file)
document.open { (success) in
if !success {
print("File not open at %#", file)
return
}
let data = populateCellData(document.fileInfo)
self.allDatas.append(data) // "buffer" list
self.tableView.reloadData()
}
document.close(completionHandler: nil)
}
} catch { ...
Then you change cellsData to a computed property like so :
var cellsData: [/*your data type*/] {
get {
return allDatas.order( /* order in accordance to your needs */ )
}
}
this way, each time a new data is added, the list is orderer before being redisplayed.
Discussion
this is the easier solution regarding the state of your code, however this may not be the overall prefered solution. For instance in your code, you reload your tableview each time you add a new value, knowing that there will be more data added after, which is not optimised.
I suggest you to read on Dispatch Group, this is a way to wait until all asynchronous operation your triggered are finished before executing certain actions (such as reloading your tableview in this case) (Readings: Raywenderlich tuts)

How to cancel async image loading for uitableviewcells on new search

I start a search on my server for profiles of users. As the user types usernames, different suggested profiles appear in a cell in a UITableView. Each profile has an image associated with it. The image is loaded asynchronously once the user types, via the cellForItem(at:) function. When the user types to search it cancels my search request to the server, however it also needs to cancel the asynchronous requests. There may be more than one request happening at a time - how should I keep track of them?
My code to download an image is like this:
model.downloadImage(
withURL: urlString,
completion: { [weak self] error, image in
guard let strongSelf = self else { return }
if error == nil, let theImage = image {
// Store the image in to our cache
strongSelf.model.imageCache[urlString] = theImage
// Update the cell with our image
if let cell = strongSelf.tableView.cellForRow(at: indexPath) as? MyTableCell {
cell.profileImage.image = theImage
}
} else {
print("Error downloading image: \(error?.localizedDescription)")
}
}
)
Note that I can add request = model.downloadImage(... if I wanted to, but request would only hold a pointer to the last time that the downloadImage() function was called.
For such purpose I recommend using Kingfisher. It's powerful 3rd party library for downloading and caching images. With kingfisher you can achieve what you want by simply calling:
// In table view delegate
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
//...
cell.imageView.kf.cancelDownloadTask()
}
You can use GCD, with iOS 8 & macOS 10.10 DispatchWorkItem was introduced, which provide this exact functionality (a task it can be canceled) in a very easy to use API.
This is an example:
class SearchViewController: UIViewController, UISearchBarDelegate {
// We keep track of the pending work item as a property
private var pendingRequestWorkItem: DispatchWorkItem?
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// Cancel the currently pending item
pendingRequestWorkItem?.cancel()
// Wrap our request in a work item
let requestWorkItem = DispatchWorkItem { [weak self] in
self?.model.downloadImage(...
}
// Save the new work item and execute it after 250 ms
pendingRequestWorkItem = requestWorkItem
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250),
execute: requestWorkItem)
}
}
In your case, you need to know through the delegate (example: TextField) when the user enters information.

UITableView reloadData not working when viewcontroller is loaded a 2nd time

I have a UITableView with custom cell displaying a list of files that can be downloaded. The cell displays the filename and download status. Everything working fine except one scenario :
The user downloads a file and navigates back to the home screen while file download in progress...
He comes back to the previous screen. File download still in progress.
File download complete. I am using tableview.reloadData() at this point to refresh the download status to "Download Complete" but reloadData() not working in this scenario. The cell label still shows "Download in progress".
Scrolling the tableview to get the cell out of screen and back refreshes the cell correctly. Anyway to do this programmatically?"
Otherwise, in normal case where user doesn't change screen, reloadData() is working fine.
Any idea how to fix this?
Thanks
I have used alamofire download with progress in the function below which is inside my UIViewController.
func DownloadFile(fileurl: String, indexPath: NSIndexPath, itemPath: String, itemName: String, itemPos: String, ItemSelected:Bool) {
let cell = myTableView.cellForRowAtIndexPath(indexPath) as! myCustomCell
let destination = Alamofire.Request.suggestedDownloadDestination(directory: .DocumentDirectory, domain: .UserDomainMask)
Alamofire.download(.GET, fileurl, destination: destination)
.progress {bytesRead, totalBytesRead, totalBytesExpectedToRead in
// This closure is NOT called on the main queue for performance
// reasons. To update your ui, dispatch to the main queue.
dispatch_async(dispatch_get_main_queue()) {
print("Total bytes read on main queue: \(totalBytesRead) / \(totalBytesExpectedToRead)")
let progress = Int((Double(totalBytesRead)/Double(totalBytesExpectedToRead)) * 100)
cell.lblMoreRuqyaFileInfo.text = "Downloading file...(\(progress)%)"
}
}
.response { _, _, _, error in
if let error = error {
print("\(error)")
cell.lblMoreRuqyaFileInfo.text = "Failed to download file. Please try again."
} else {
cell.lblMoreRuqyaFileInfo.text = "File Downloaded sucessfully"
//reloadData() not working from here
self.myTableView.reloadData()
}
}
}
The above func is being called in the tableview's editActionsForRowAtIndexPath below.
func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
if myTableView.cellForRowAtIndexPath(indexPath) == nil {
let action = UITableViewRowAction()
return [action]
}
let cell = myTableView.cellForRowAtIndexPath(indexPath) as! myCustomCell
let fileurl = cell.textLabel!.text
let ruqyainfo = cell.lblMoreRuqyaFileInfo.text
let sItemPath = cell.lblCompiledRuqya.text! + "->" + cell.textLabel!.text! + "->\(indexPath.section)->\(indexPath.row)"
let sItemName = cell.lblCompiledRuqya.text!
let sItemPos = "->\(indexPath.section)->\(indexPath.row)"
var bItemSelected:Bool = false
if myTableView.cellForRowAtIndexPath(indexPath)?.accessoryType == UITableViewCellAccessoryType.Checkmark {
bItemSelected = true
} else {
bItemSelected = false
}
//check if file already downloaded,return empty action, else show Download button
if ruqyainfo?.containsString("Download") == false {
let action = UITableViewRowAction()
return [action]
}
let line = AppDelegate.dictCompiledRuqya.mutableArrayValueForKey(AppDelegate.dictCompiledRuqya.allKeys[indexPath.section] as! String)
let name = line[indexPath.row].componentsSeparatedByString("#")
let DownloadAction = UITableViewRowAction(style: .Normal, title: "Download\n(\(name[3]))") { (action: UITableViewRowAction!, indexPath) -> Void in
self.myTableView.editing = false
AppDelegate.arrDownloadInProgressItem.append(name[0])
self.DownloadFile(fileurl!, indexPath: indexPath, itemPath: sItemPath, itemName: sItemName,itemPos: sItemPos, ItemSelected: bItemSelected)
}
DownloadAction.backgroundColor = UIColor.purpleColor()
return [DownloadAction]
}
//Objective C
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
[self.yourTableView reloadData];
}
//Swift
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.yourTableView.reloadData()
}
you can use delegates so that the download controller can tell the first controller that the download is complete, and that it should redraw the tableView - or at least the indexPath that has just completed.
set up the download controller like this
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)
{
let destinationController : DownloadController = segue.destinationViewController as! DownloadController
destinationController.delegate = self
destinationController.indexRowToRefresh = currentlySelectedIndexRow
}
and then in the download completion closure, add something like this
delegate.refreshTableRow(indexRowToRefresh)
and in your first controller, implement the delegate method to refresh this row (or the whole table)
func refreshTableRow(indexRowToRefresh : Int)
{
var indexPath = NSIndexPath(forRow: indexRowToRefresh, inSection: 0)
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Top)
}
Hey #John Make sure your TableView datasource is reflected with file upload status permanently.I think, when you complete file uploading you change status in tableview datasource
1. First scenario as you are switching to second view controller, and coming back to previous view, it might be reinitializing your datasource. Data source might wasn't permanently reflected.2. In normal scenario, as you are on same view controller(not switching to other). Tableview datasource might be not reinitialized holding updated file upload status.
I suggest to save user file download status saved at some persistant storage or on web server. That doesn't leads to inconsistency in data.
It could be a thread related issue (aka you're coming back from the download thread, not on the UI thread, hence the data is refreshed but not displayed).
Try using this on selfand pass it a selector that refreshes your tableview.
performSelectorOnMainThread:
You used :
let cell = myTableView.cellForRowAtIndexPath(indexPath) as! myCustomCell
and set text for lblMoreRuqyaFileInfo :
cell.lblMoreRuqyaFileInfo.text = "File Downloaded sucessfully"
so, you don't have to call self.myTableView.reloadData()
Try:
dispatch_async(dispatch_get_main_queue()) {
cell.lblMoreRuqyaFileInfo.text = "File Downloaded sucessfully"
}
p/s and where did you call functionDownloadFile , show it, plz!

How to reload tableview after adding new entry?

I am creating a cloudkit tableview. I load the app and my tableview appears with my entries from cloud kit.
I then use my add method insertNewObject which adds the record to cloud kit but this does not show up in my tableview. It will only show up on my next run of the app.
func insertNewObject(sender: AnyObject) {
let record = CKRecord(recordType: "CloudNote")
record.setObject("New Note", forKey: "Notes")
MyClipManager.SaveMethod(Database!, myRecord:record)
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
This is my add method. I am calling tableview reload as you can see but nothing is happening.
My tableview creation code:
// Tableview stuff --- Done
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
/////// Get number of rows
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
//// FIll the table
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
let object = objects[indexPath.row]
cell.textLabel!.text = object.objectForKey("Notes") as? String
return cell
}
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
As requested: Method that saves to CloudDB
func SaveMethod(publicDatabase: CKDatabase, myRecord: CKRecord ) -> CKRecord {
publicDatabase.saveRecord(myRecord, completionHandler:
({returnRecord, error in
if let err = error {
self.notifyUser("Save Error", message:
err.localizedDescription)
} else {
dispatch_async(dispatch_get_main_queue()) {
self.notifyUser("Success",
message: "Record saved successfully")
}
}
}))
return myRecord
}
My viewdidload method in masterview:
override func viewDidLoad() {
super.viewDidLoad()
// Database loading on runtime
Database = container.privateCloudDatabase
///Build Query
let query = CKQuery(recordType: "CloudNote", predicate: NSPredicate(format: "TRUEPREDICATE"))
///Perform query on DB
Database!.performQuery(query, inZoneWithID: nil) { (records, error) -> Void in
if (error != nil) {
NSLog("Error performing query. \(error.debugDescription)")
return
}
self.objects = records!
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
You should not reload your entire tableView when you insert a single object. Only do that when you know ALL the data has changed.
To do what you want, this is the order:
Insert a new data object into your datasource (self.objects). Make sure you get the index of where it ends up in the array.
Call insertRowAtIndexPath: with the correct indexPath on your tableView. This will make sure your data and tableView are in sync again, and tableView:cellForRowAtIndexPath: is called for at least your new data object (and possible others, as certain cells might now be reused to display other data).
Note that the order is always: update your data first, then update your UI (the only place I know of that his is hairy is when using a UISwitch).

Swift 2 + Parse: Array index out of range

SOMETIMES THE REFRESH WORKS SOMETIMES IT DOESN'T
I have a UITableViewController which is basically a news feed. I have also implemented a pull to refresh feature. However sometimes when I pull to refresh it gives me the error
'Array index out of range'.
I know this means an item it is trying to get does not exist but can you tell me why? Here is my code:
override func viewDidLoad() {
super.viewDidLoad()
refresher = UIRefreshControl()
refresher.attributedTitle = NSAttributedString(string: "Pull to refresh")
refresher.addTarget(self, action: "refresh", forControlEvents: UIControlEvents.ValueChanged)
self.tableView.addSubview(refresher)
refresh()
tableView.delegate = self
tableView.dataSource = self
}
and the refresh() function:
func refresh() {
//disable app while it does stuff
UIApplication.sharedApplication().beginIgnoringInteractionEvents()
//get username and match with userId
let getUser = PFUser.query()
getUser?.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if let users = objects {
//clean arrays and dictionaries so we dont get indexing error???
self.messages.removeAll(keepCapacity: true)
self.users.removeAll(keepCapacity: true)
self.usernames.removeAll(keepCapacity: true)
for object in users {
if let user = object as? PFUser {
//make userId = username
self.users[user.objectId!] = user.username!
}
}
}
})
let getPost = PFQuery(className: "Posts")
getPost.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if error == nil {
if let objects = objects {
self.messages.removeAll(keepCapacity: true)
self.usernames.removeAll(keepCapacity: true)
for object in objects {
self.messages.append(object["message"] as! String)
self.usernames.append(self.users[object["userId"] as! String]!)
self.tableView.reloadData()
}
}
}
}
self.refresher.endRefreshing()
UIApplication.sharedApplication().endIgnoringInteractionEvents()
}
and:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let myCell = tableView.dequeueReusableCellWithIdentifier("SinglePostCell", forIndexPath: indexPath) as! PostCell
//ERROR GETS REPORTED ON THE LINE BELOW
myCell.usernamePosted.text = usernames[indexPath.row]
myCell.messagePosted.text = messages[indexPath.row]
return myCell
}
You have a race condition given you are doing two background tasks, where the second depends on values returned from the first. getUser?.findObjectsInBackgroundWithBlockwill return immediately, and getPost.findObjectsInBackgroundWithBlock will start executing. The getPost should be inside the block for getUser, to ensure the sequence is correct.
Similarly, the following two lines should be inside the second block:
self.refresher.endRefreshing()
UIApplication.sharedApplication().endIgnoringInteractionEvents()
Given the error line, you probably also have a race condition between the two background tasks and displaying the tableView. I would be inclined to try:
func tableView(tableView:UITableView!, numberOfRowsInSection section:Int) {
return self.refresher.refreshing ? 0 : self.usernames.count
}
This way you won't touch self.usernames until the background refresh is finished (as long as you remember to put endRefreshing inside the second block, which is also put inside the first block).
I Believe that in self.users[user.objectId!] = user.username! the user.ObjectId is some random value assigned by parse which looks like this: "34xcf4". This is why you might be getting 'Array index out of range'.
There are two required methods for configuring a UITableView:
tableView(_:cellForRowAtIndexPath:)
and
tableView(_:numberOfRowsInSection:)
In your code you are presenting only one required method, if you don't implement the second method then it that may cause errors.
Check the documentation at:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableViewDataSource_Protocol/#//apple_ref/occ/intfm/UITableViewDataSource/tableView:cellForRowAtIndexPath:
You are calling self.tableView.reloadData() on every addition to your array and doing so in a background thread.
As a general rule, you should not do UI updates in a background thread. When you clear self.messages and self.usernames, because you are in background thread, nothing prevents the tableview from trying to get a cell at an index that no longer has any data in the array.
If you want to keep your code in the background thread (risky as it may be), you should at least call .beginUpdates before reloading your arrays and wait until they're all done before calling reload and endUpdates.

Resources