iOS - loading items when user reach the scroll view bottom - ios

I'm currently using CloudKit as my backend and so far I've been enjoying it pretty much.
I've a query happening, that retrieves data to populate a table view.
Since I don't know the items count that it may have, I'm filtering the query to X number of items (let's say 15).
My goal is that when the user scrolls down to the bottom (last queried item) of the table view ill query the backend to continue filling the table view.
I've searched but couldn't find CloudKit code that does this.
Can somebody give me a hint on how to do it?
Thank you all for the given help.
Best, Ivan.

On IOS I think you have to use UITableViewController or just UITableView.
Add a UIActivityIndicatorView (i.e. spinner) to your UITableViewController. Connect the outlet to the code:
#IBOutlet weak var spinner: UIActivityIndicatorView!
Add a property to your UITableViewController to keep track that you're currently loading more data so that you don't try to do it twice:
var loadingData = false
Start the spinner animating and then call refreshRes():
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if !loadingData && indexPath.row == refreshPage - 1 {
spinner.startAnimating()
loadingData = true
refreshRes()
}
Have refreshRes() run on a background thread. This will allow your table to still move freely. The animated spinner will tell the user that more data is coming. Once your query returns, update the table data on the main thread.
func refreshRes() {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
// this runs on the background queue
// here the query starts to add new 15 rows of data to arrays
dispatch_async(dispatch_get_main_queue()) {
// this runs on the main queue
self.refreshPage += 15
self.tableView.reloadData()
self.spinner.stopAnimating()
self.loadingData = false
}
}
After that it's depend of the server result you have to make a request to GET the others 15 datas

Related

Who is calling reloadData method for UITableView

It can sounds weird but I don't understand why my tableView is showing cells.
I got array of items that should be shown in cells but I don't run reloadData method of my tableView anywhere in my code. It seems that some of app components or maybe frameworks inside app is calling reloadData method and I want to find out which one?
How it can be done?
A table view loads itself the first time it is added to the window hierarchy. You don't need an explicit call to reloadData for the table to load itself initially.
If you want to see how this is really done, put a breakpoint on your table view data source methods and bring up your table view. Look at the stack trace in the debugger to see the sequence of events.
If your data preparation takes some time and you do not want the table view to show any data initially you could use an approach like this:
class TableViewController: UITableViewController {
var someDataSource: [Any]!
var dataSourcePrepared = false {
didSet {
tableView.reloadData()
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
guard dataSourcePrepared else { return 0 }
return someDataSource.count
}
func doSomePreparationStuff() {
// ...
// ...
someDataSource = ["Some", "Content"]
dataSourcePrepared = true
}
}
In this case I used a Bool variable dataSourcePrepared which is false initially. As soon as you have prepared your content set it to true and the table view gets reloaded.

Pass the result to a Button within a UITableViewCell after executing an on tap closure?

I'm looking for any possible way of passing the result of an networking update to a button in a UITableViewCell as a closure.
I have some UITableViewCells that are products. In these cells, I have an 'Add to Cart' button. I set a buttonTap closure in my UITableViewCell cellforRowAtIndexPath method, setup a touch handler within the cell for the button, and when that handler is called, execute the buttonTap closure. I handle my cart updating on a cart object which lives on the main controller.
The result of the cart update action returns true if they can add more items to their cart. Then, I update the button accordingly. I like this approach because I don't have to deal with delegates and I can keep all of the cart logic itself far far away from the cell; the cell just knows how to make a button enabled/disabled/loading/etc.
/// Buttom tap callback callback.
public typealias Selection = () -> Bool
class MealTableViewCell: UITableViewCell {
var buttonTap: Selection?
// Runs when tapping the button
func didTapAdd() {
if let buttonBlock = buttonTap {
self.button.isLoading = true
// Simulate loading
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
self.button.enabled = buttonBlock()
self.button.isLoading = false
}
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = .... (fetch cell)
cell.buttonTap = {
// returns true if the user can add more items to their cart
return self.cart.update(product: product, quantity: 1)
}
}
My question is that currently, my cart is all local with no API calls. I'm currently switching the cart over to an API driven one, with network calls to add and remove items. That means I can no longer return a BOOL, or return at all, from my cart.update(product:,quantity:) method as it is now an async call.
So, I can do something like rewrite that method signature to be
self.cart.update(product: product, quantity: 1, success: { canAddMore in
// API call succeeded
}, failure: { error in
// fall failed
})
The question is that how can I pass canAddMore to the tableViewCell? If I redefine what Selection means to take in a block that takes a bool as a param, I can't pass that param in from the controller as it would only be passed in when the block is executed on the cell itself.
How can I do something like
cell.buttonTap = {
cell.buttonTap = {
self.cart.update(product: product, quantity: 1), success: { canAddMore in
// !!!! What can I call here to pass canAddMore to the cell.
}, failure: { error in
}
}
}
canAddMore can be any value really, a BOOL is just this example. My big goal is to avoid coupling any knowledge of the cell's loading and buttons to the controller itself. If I use delegates, I would have to have a two way delegate makes the cell a delegate of the controller, and I've always felt that's the sort of wrong direction to approach this. I'm not positive it's really possible to pass the result of a closure back to the cell, but I am hoping there is!
EDIT: The big question I'm really trying to answer is if it is at all possible to pass data back to the cell (or any object) that originally called closure through that closure. There's a million ways to do data modifications in a table view, but that's sort of the main thing I'm trying to address.
I'm also "looking" to avoid storing the canAddMore state (e.g. the quantity remaining for that product) in the main 'products' array that powers the tableview. The initial state is set there, returned from a /products endpoint, but after that, inventory being available or not is returned by the carts API action.
I don't think you want to do what you think you want to do :)
In a nutshell, instead of trying to "talk back" to the cell that called the closure, you probably want to track the "canAddMore" state of each product in your Products data array, and then update the table row(s) when the state changes. So...
User taps "Add to Cart"
Give visual feedback in that row to show that you are processing the tap (gray out the button, or show a spinner, whatever looks good)
Call back to the closure to start the Add-to-cart API call
When the API call returns, update your local Products data to indicate the "canAddMore" state
reload the row(s) in the table to update the Button (make it active, inactive, change the title, whatever)
You almost certainly need to be doing something similar anyway, so the Buttons in each row will be updated when the user scrolls and the cells are reused.
A general approach is to update the cell's content with tableView.reloadRows(at: [indexPath], with: true) in your callback. This will call func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell for the specified cell. Don't forget that this has to be done in the main queue.

How to reload UITableView data avoiding freeze

Some with experience can tell me what is the best way to execute a reloadData() from a UITableView on swift avoiding freeze?
I have a ViewController with a TableView, this shows a list of users in pairs of 10 rows. When the scroll show the last row - 1, in a background, the app request the next 10 users and then they are added to the rest of users for show now 20 users in the TableView.
When this is executed with a delegated method, the reload causes a freezing around 1~2 seconds and hasn't a comfortable navigation.
Any idea to solve this?
When new data coming, you don't need to reload the whole tableView. You just need to insert new rows accordingly. That won't cause any lag/freeze.
func didFinishLoadNewUsers(newUsers: [User]) {
tableView.beginUpdates()
//array of index paths for new rows at the bottom
var indexPaths = [NSIndexPath]()
for row in (currentUsers.count..<(currentUsers.count + newUsers.count)) {
indexPaths.append(NSIndexPath(forRow: row, inSection: 0))
}
//update old data
currentUsers.appendContentsOf(newUsers)
//insert new rows to tableView
tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
tableView.endUpdates()
}
If I understand your question correctly, your symptom is this:
The user scrolls to the bottom.
Scrolling is halted (because the bottom was reached).
Your controller notices that the user reached the bottom and starts downloading more rows.
There is an ugly delay while new rows finish downloading.
You insert the new rows, and now the user can resume scrolling.
The user is disappointed with your app now, because it stopped scrolling and delayed for no apparent reason. I believe this is what you meant by "freezing" and "uncomfortable navigation".
Solution:
Don't wait until the last row is displayed! For example: start with 40 rows and download 40 more rows when scrolling reaches a distance of about 15 rows from the bottom. That way, there's a good chance the download will finish quickly enough that it will look perfectly smooth to the user.
If you want to get really fancy, you can take scroll speed, row height, and server latency into account. But in my experience, none of that is really necessary for a smooth "infinite scrolling" experience.
With all due respect, you and the other responder are wrong to think that "reloading the whole table view" is responsible for this. UITableView.reloadData() is actually seamless (if the user hasn't reached the bottom yet).
Try this:
var shouldDownloadMoreRows : Bool {
get {
// This should return false if the server tells us there are no more rows.
// For example, if our last request for 40 got less than 40 rows, then we
// can probably assume there are no more.
// It should also return false if a request is currently in progress, or a
// request failed within the last 0.5 seconds or so, or if the controller
// is quitting (about to animate away).
return ...
}
}
func downloadMoreRows() {
...
// After the download finishes
didFinishDownloadingMoreRows()
}
func didFinishDownloadingMoreRows() {
// This will be smooth. It will not disrupt scrolling or cause any freezing or lag.
self.tableView.reloadData()
}
func tableView(tableView: UITableView,
willDisplayCell cell: UITableViewCell,
forRowAtIndexPath indexPath: NSIndexPath) {
let numRowsInSection = tableView.numberOfRowsInSection(indexPath.section)
if self.shouldDownloadMoreRows && indexPath.row + 15 >= numRowsInSection {
self.downloadMoreRows()
}
}

How to detect when UITableView has finished loading all the rows? [duplicate]

This question already has answers here:
How to detect the end of loading of UITableView
(22 answers)
Closed 6 years ago.
I need to call a function after the UITableView has been loaded completely. I know that in most cases not every row is displayed when you load the view for the first time, but in my case, it does as I only have 8 rows in total.
The annoying part is that the called function needs to access some of the tableView data, therefore, I cannot call it before the table has been loaded completely otherwise I'll get a bunch of errors.
Calling my function in the viewDidAppear hurts the user Experience as this function changes the UI. Putting it in the viewWillAppear screws up the execution (and I have no idea why) and putting it in the viewDidLayoutSubviews works really well but as it's getting called every time the layout changes I'm afraid of some bugs that could occur while it reloads the tableView.
I've found very little help about this topic. Tried few things I found here but it didn't work unfortunately as it seems a little bit outdated. The possible duplicate post's solution doesn't work and I tried it before posting here.
Any help is appreciated! Thanks.
Edit: I'm populating my tableView with some data and I have no problems with that. I got 2 sections and in each 4 rows. By default the user only sees 5 rows (4 in the first section, and only one in the second the rest is hidden). When the user clicks on the first row of the first section it displays the first row of the second section. When he clicks on the second row of the first section it displays two rows of the second section, and so on. If the user then clicks on the first row of the first section again, only one cell in the second section is displayed. He can then save his choice.
At the same time, the system changes the color of the selected row in the first section so the users know what to do.
Part of my issue here is that I want to update the Model in my database. If the users want to modify the record then I need to associate the value stored in my database with the ViewController. So for example, if he picked up the option 2 back then, I need to make sure the second row in the first section has a different color, and that two rows in the second sections are displayed when he tries to access the view.
Here's some code :
func setNonSelectedCellColor(indexPath: NSIndexPath) {
let currentCell = tableView.cellForRowAtIndexPath(indexPath)
currentCell?.textLabel?.textColor = UIColor.tintColor()
for var nbr = 0; nbr <= 3; nbr++ {
let aCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: nbr, inSection: 0))
let aCellIndexPath = tableView.indexPathForCell(aCell!)
if aCellIndexPath?.row != indexPath.row {
aCell?.textLabel?.textColor = UIColor.blackColor()
}
}
}
func hideAndDisplayPriseCell(numberToDisplay: Int, hideStartIndex: Int) {
for var x = 1; x < numberToDisplay; x++ {
let priseCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: x, inSection: 1))
priseCell?.hidden = false
}
if hideStartIndex != 0 {
for var y = hideStartIndex; y <= 3; y++ {
let yCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: y, inSection: 1))
yCell?.hidden = true
}
}
}
These two functions are getting called every time the user touches a row :
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let path = (indexPath.section, indexPath.row)
switch path {
case(0,0):
setNonSelectedCellColor(indexPath)
hideAndDisplayPriseCell(1, hideStartIndex: 1)
data["frequencyType"] = Medecine.Frequency.OneTime.rawValue
case(0,1):
setNonSelectedCellColor(indexPath)
hideAndDisplayPriseCell(2, hideStartIndex: 2)
data["frequencyType"] = Medecine.Frequency.TwoTime.rawValue
case(0,2):
setNonSelectedCellColor(indexPath)
hideAndDisplayPriseCell(3, hideStartIndex: 3)
data["frequencyType"] = Medecine.Frequency.ThreeTime.rawValue
case(0,3):
setNonSelectedCellColor(indexPath)
hideAndDisplayPriseCell(4, hideStartIndex: 0)
data["frequencyType"] = Medecine.Frequency.FourTime.rawValue
default:break
}
}
I store the values in a dictionary so I can tackle validation when he saves.
I'd like the first two functions to be called right after my tableView has finished loading. For example, I can't ask the data source to show/hide 1 or more rows when I initialize the first row because those are not created yet.
As I said this works almost as intended if those functions are called in the viewDidAppear because it doesn't select the row immediately nor does it show the appropriate number of rows in the second sections as soon as possible. I have to wait for 1-2s before it does.
If you have the data already that is used to populate the tableView then can't you use that data itself in the function? I am presuming that the data is in the form of an array of objects which you are displaying in the table view. So you already have access to that data and could use it in the function.
But if that's not the case then and if your table view has only 8 rows then you can try implementing this function and inside that check the indexPath.row == 7 (8th row which is the last one).
tableView(tableView: UITableView, didEndDisplayingCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath)
Since all your rows are visible in one screen itself without scrolling you could use this function to determine that all the cells have been loaded and then call your function.

Strange behaviour with core data entity

I have this portion of code
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let itemID:NSManagedObjectID = self.frc!.objectAtIndexPath(indexPath).objectID!
let entity:Entity = self.frc?.managedObjectContext.existingObjectWithID(itemID, error: nil) as Entity
if entity.completed {
entity.completed = false
} else {
entity.completed = true
}
println(entity.completed)
}
when the event occurs tableView performs scroll to top animation any ideas?
You are probably updating your managed object array somehow (not shown), perhaps via some delegate method. Most likely, you are calling tableView.reloadData() somewhere to update the displayed data. That automatically reloads the entire table view and will scroll to the top.
Instead, just update the cell in question with tableView.reloadRowsAtIndexPaths(rowAnimation:) documented here.
Maybe you sorting routine (not shown) is sorting by the completed attribute. Then the scrolling might be expected behavior.
NB: It seems odd that you keep an array of IDs as the dataSource. It would be just as efficient to just have an array of objects, due to the faulting behavior of Core Data. Better even, you should be using a NSFetchedResultsController with a table view - you get lots of cool behavior for free and are guaranteed to have the best solution for performance and memory management.

Resources