Animate progress on 100s of UIButtons without blocking main thread - ios

I have a UITableView with at most a few hundred cells (not all visible at once). Each cell contains a UIButton with a way to indicate progress of an upload. A URLSession performs the uploads in background tasks.
Currently, the session delegate is the UIViewController that manages the cells. As a result, the session calls delegate
.URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend: to periodically inform the delegate of the progress of sending content to the server.
In the delegate method, I find the UIButton associated with this task and animate the new progress (I can find the button because I make button.identifier = task.identifier).
This approach forces me to find the button every time the delegate method is called. This seems indirect and I am wondering if there is a better way to do this — there could be 100s of buttons so worried about runtime.
I was thinking to make the button be the session delegate, but that goes against MVC and the button reference may disappear or change in a table view causing undefined behavior (though it sort of makes sense to only update buttons that are actually in memory).

there could be 100s of buttons
No, there couldn't. Cells that do not appear on the screen do not exist at all (because cells are reused in a table view). So you only need to worry about the cells that are actually visible at any one moment. See UITableView visibleCells and indexPathsForVisibleRows. Thus, even though your approach is not extremely efficient, it isn't extremely inefficient either.
However, the correct way to do this is to use the progress object vended by your upload task. When the upload starts, tell the cell or the button or whatever to start observing the progress object's fractionCompleted using key-value observing. Now the cell or the button or whatever is in direct contact with that one task and can update itself every time it hears that the fractionCompleted has changed. When the cell stops being displayed, stop observing. There's a little more to it (i.e. to cope with reused cells that scroll onto the screen when the corresponding task is already in progress) but that's the basic architecture you want.

Related

How can I insert section/items into a UICollectionView rapidly without blocking the main thread/UI?

I have a process that runs in the background that is adding a lot of Realm records for a couple minutes. I also have a UICollectionView which has a datasource hooked up to a List of those Realm records. I have a Realm notification hooked up to call insertItems(at:) when new records are added.
The problem is that since there are so many insertions back-to-back, the main thread is almost always waiting on the insertItems(at:) call to finish. Therefore, the app is completely unresponsive until the insertions finish.
First, it seems strange to me that insertItems(at:) is taking so long, since only maybe the initial 0.05% of the records are even visible, so I would have thought that UIKit would be optimized to not do a bunch of work for cells off-screen. It's calling collectionView(_:layout:sizeForItemAt:) for every cell, even when they're off screen. I'm surprised that UIKit bothers to call this delegate method on cells which are not on screen or even close to being on screen. The only reason I can think of needing to know the size of cells so far down below the currently visible cells is so that the scroll indicator can be sized and animated more accurately.
Since apparently UICollectionView is doing work for every IndexPath insertion even when not necessary (I'm hoping I'm wrong), I'm wondering if anyone has a suggestion for how to structure it such that the records can still be added as quickly as possible while not blocking the UI. Maybe there's a way to structure this differently with GCD or something?

Reload data in table view without harming animation

I have a UITableView-based in-game shop.
Every cell has a "BUY" button which is mostly enabled and can be switched to "BOUGHT" if the item is a one-time purchase or can be disabled if there are not enough money.
Right now what I do is calling reloadData every time buy button is being pressed in order to update visible cells and the current cell itself. (I have to update all cells, because after purchase it is possible that there wont be enough money for visible item cells).
But it causes weird animation glitches, like when I click on one cell's buy button and animation finishes on another one.
I think this happens due to reusability of cells. So what I want to know is how to reload data in the whole table view without harming native animation.
The only thing I can think of is not to use reusable cells and cache them all, but I dont think this is a good programming practice.
First, make sure that your view layer and model layer are separate. There should be some non-view object that knows about each item; we'll call it Item.
Now, create an ItemCell (you probably have one already). That's your reusable cell. Hand it the Item. It should configure itself based on the data in there.
Use KVO, delegation, or notifications to let the cell observe its item. When the Item changes its status, the cell should update its own button.
When your cell is reused, you'll pass a new item to it. It should stop observing the previous one, and start observing the new one (and of course reconfigure itself to match the current status).
By separating the views (which are reusable and transitory) from the model (which is stable and long-lived), you get the performance benefits of cell reuse with correct animations and no need to call reloadData.
You could have a reloadCell method in the cell class and loop through the table's visibleCells and update their UI that way. That way they are not recreated (or re-used), they just have their relevant UI pieces that could have changed due to the new data updated.

Downloading images only for visible UITableViewCells

In UITableViewController I have custom cells with thumbnail image which is cached by me using TMCache. So the basic workflow of loading cells is:
Fill cell lables with data from model
Check if I have a thumbnail image cached
If yes, than get it from cache...
If not, download it from web...
And me concerns are that when I don't have anything in cache I'd start downloading a lot of images (even if I have set maximum number of concurrent tasks) so when the user scrolls for example a 100 rows my tasks array in AFHTTPSessionManager will be dealing with all of then even if user is not interested in many of them.
So I came with this solution:
When usere scrolls down and downloading begins, but in a moment this cell gets off the screen, I want to cancel NSURLSessionDataTaskfor this cell. But... I don't know how to check which cell should cancel its task and the more important issue, what if task is completed in 90% and I cancel it (waste of data transfer)? I've noticed that in Facebook app they're not cancelling those tasks because when you scroll up they are loaded.
I wonder if this is a good approach or maybe I'm trying to overcomplicate everything?
Check out the UITableViewDelegate Protocol Reference.
Specifically, you can use the following methods to track cells' appearance and disappearance:
– tableView:willDisplayCell:forRowAtIndexPath:
- tableView:didEndDisplayingCell:forRowAtIndexPath:
Why waste bandwidth? Do this:
in UITableViewControllerDataSource::cellForRowAtIndexPath: start a timer to go off in, say 200 ms or something, which will begin the download process when it's triggered. Associate the timer with the indexPath (or cell).
In UITableViewControllerdelegate::tableView:didEndDisplayingCell:: kill the timer if it hasn't already gone off.
There is a method of UITableViewCell that you can override: prepareForReuse. It will get called when table view is no longer need this cell and reusing memory for another cell that is becoming visible.
I think it will be a good start for you to cancel request associated with this cell.

Cancel NSOperations based on position in TableView/CollectionVIew

Suppose I have a table view or a collection view. In the cells I would need to load certain content (eg. an Image). I can get the content from local storage (if it has been saved there) or from the internet. Either way loading the content takes a while (500 kB Image).
The best way to do this is to create an NSOperation that will load in the background and then call a delegate when it finishes.
But suppose the user enters the table view / collection view and the queues start to get the first set of content (1-10) but then the user scroll quickly right to the end (visible cells 100-110) what should I do? The start-download operation happens when the cell is presented (cellForRowAtIndexPath) so I need to wait for everything to download until the user (who is now at the end of the tableView) sees content.
I tried creating a queue on each cell and cancelling that in prepareForReuse, but that crashes the app.
Anyway if the user is in a section of the tableview and content is downloading, but not shown yet, and scrolls further causing the download to be cancelled and then returns. Then he will need to download the first half of the image again, which is not good on limited data plans.
What is the best way to handle such a situation?
You could have your controller class adopt the UITableViewDelegate protocol, which will allow you to inject custom code into –tableView:willDisplayCell:forRowAtIndexPath: (called for you by the table view when a cell is to be displayed). In it you could calculate that because of the screen size, only rows within k indices of any row being shown could possibly be visible; thus, in this method call, cancel all the load operations associated with rows outside that range (relative to indexPath argument).

What is a better way to deal with data after an async call using NSURLConnection?

This is the current logic that I'm using to populate a table view with NSURLConnection. It doesn't seem elegant to me.
Table View Controller's viewDidLoad method calls "sendConnection" method in my api wrapper class with the URL string as a parameter. This method makes the NSURLConnection. In connectionDidFinishLoading (which is in my wrapper class), another method is called (also in the wrapper class) with the connection as a parameter. This method extracts the URL from the connection object and examines it. It then uses a switch statement to deal with the data depending on the URL. The data is stored in variables in the wrapper class itself. By the time cellForRowAtIndexPath is called, the async call has finished and the data has been processed.
Is there a better way of doing this?
My reason for asking this is as follows:
I want to refresh a cell with a new height and a new text label when it is clicked. The data for this text label will be retrieved from the server upon the cell being tapped. Each cell will have slightly different data in the label (each cell represents a 'user' and the label will display how many mutual friends you have with the user). I want to store the data in the cell itself when the data is retrieved and then place it into the text label. This doesn't seem possible with my current way of making URL calls.
Any help with how to achieve this would be appreciated.
Here is some pseudo code for a pattern I like to use in these situations. Maybe it will help you as well.
- (void)viewDidLoad {
//1. put up some type of progressHud or spinner
//2. call your NSURL wrapper
//3. in the completion block of your wrapper, set your datasource variables
//example: #property (nonatomic,strong) NSArray *listOfData;
//4. create a custom setter for your datasource that calls tableview reload
//5. enable a refresh function; like "pull to refresh" or a bar button
//6. when pull to refresh is tapped or called, just repeat these steps
}
- (void)setListOfData:(NSArray*)listOfData {
_listOfData = listOfData;
if (_listOfData) {
[self.tableView reloadData];
}
}
As I read your question again, here are a couple more thoughts:
the pattern above will work for your initial load, to create the list of people or friends, etc.
If you plan on making another round trip after the cell is tapped, then you have to consider a number of issues. This is similar to a common problem with lazy loading images into tableview cells. There are issues like scrolling to consider - what if the cell is scrolled off the view before the data returns, for example, what if the cell has been reused, now the data is not tied to that cell any longer.
There are many async image libraries available on Github that would be good to look at to see how they solved those issues. Generally they are keeping track of the item in the cell and then checking if the cell is still in view and if so, they set the image.
You have a similar issue to solve. Tap the cell, get the new data, then update the cell. Resizing the cell will require you to reload it.
Look into [tableview reloadRowsAtIndexPaths:(NSArray*) with RowAnimation:(UITableViewRowAnimation)];
hope that helps
best wishes;
You should have a "Data Model" which represents the content (that is the cells) of your Table View.
Since you have "rows" in your table view, it makes sense this data model is a kind of array (possibly a NSArray) whose elements keep the data and state of the cell.
The data for each cell should not only have all the "data" properties rendered in your cell (e.g. the label) but also its state:
When a user tabs on a cell it will start an asynchronous task. This task may take a while to finish since it fetches data from a remote server. Think of several seconds, or even longer. You need to keep track of pending update tasks, since your implementation should prevent the user to update a cell again before the corresponding pending update task has been finished.
There are several techniques to accomplish this. One way is to have a property in your "Cell Data" class which reflects this state, for example:
#interface CellModel : NSObject
#property (atomic) BOOL hasPendingUpdate;
...
When the cell will be rendered, you retrieve the value of the property and render the cell appropriately.
When the update task finishes, it updates its cell model data.
This model update will eventually update your Table View. There are several techniques to accomplish this. You should take care about thread-safety here and the "synchronization" of your Data Model and the table view cells. For example ensure the value of the hasPendingUpdate only changes on the main thread - since otherwise your rendered cell may become out of sync with the data model (not to mention race conditions in case you modify and access the property on different threads without synchronization primitives).
While the cell waits for an update, it should visually indicate this state (using a spinner for example) and disable the action to start an update task.
Very much recommended is a "Cancel" button, which either cancels a certain cell update task or all pending update tasks.
When the user moves away from this view, you may consider to cancel all pending tasks.

Resources