Reloading UITableView data according to slider value - ios

I'm trying to implement a table view which should display cells according to the value of a UISlider.
Example: The table is empty. The user quickly drags the slider up to a value of 400. Now the table should contain 400 rows.
The data the cells should show is stored in core data and has about 400 entries. We're trying to achieve a cool effect when dragging the slider, so you can visually see each cell being added (if you're viewing the part of the table view where the cell would end up, of course). The built in animations supplied when using - (void) reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation are sufficient, and the effect looks cool when going between 1 to 10 rows.
The problem: After the value has reached 10 or more it starts to lag too much to work. I think it's because reloadSections: has to check every cell every time the slider changes value and I loose too much precious time.
So, I'm looking for ideas on how to implement this. Should I use an NSFetchedResultsController and change the fetch request every time the slider changes value (which could happen 20 times per second)? Should I have all the data loaded into an array and just reuse cells the standard way?
I've tried both ways and the lag is pretty much the same. I'm thinking the problem might be that reloadSection: is too slow. I'm thinking that - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation would be a better choice, but I'm looking for more ideas on how to implement this.
Any help would be appreciated!
The last code I tested when the slider changes value:
- (IBAction) sliderChangedValue:(UISlider *)slider
{
// Make sure we only run this function on integer changes. It fires too often on float changes.
if ((int)slider.value == lastSliderIntegerValue) {
return;
}
if (slider.value > lastSliderIntegerValue) {
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:(int)slider.value inSection:0]]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
else {
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:(int)slider.value inSection:0]]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
lastSliderIntegerValue = (int)slider.value;

I'm not incredibly experienced with iOS development, but I have done a lot of game dev. It seems like the root of your problem is the amount of animation that your program has to render. Since you said that your motivation for this program is preserving the cool effect, then you should change everything possible to optimize your program up to the point the user can perceive. I understand that isn't very clear, so here's a possible solution:
You shouldn't be animating the addition of every single row for every speed at which the user can move the slider. Instead, only animate as many as you need to for the experience to be smooth. This can be accomplished by calculating the rate at which the slider is being changed, and then adjusting how many rows are added to the table before reloadData is called. Only animate the addition of the last row in the cycle.
Because I don't know what system you're developing for or really much at all about the graphical power of the various iOS platforms, I can't really give you specific values, but you should play around with different settings and see what works.

Related

Modern Era UITableView Animation

In a UITableView what would be the most convenient way of showing up additional information from a smooth scrolling effect?
You might want to look at the screenshot below. The top UITableViewCell is a kind of "Create new stuff"-button field, and I would like a formular to show up from below this field with a smooth uniform scroll animation.
How is this best achieved? Do I want to create a view and manipulate with the current cell's height and animate+display a SubView or do I want to manipulate with the Delegate and DataSource to simply create a condition on showing more cells if the button is pressed (which causes a reloadData with some kind of animation set true) ?
I could wander around in the wilderness and mess around, but I guess that I'm not the first one to encounter such issue and that someone might have some constructive input.
** UPDATE **
All the interesting stuff happens in indexPath.section = 0 so I now simply created an (atomic) BOOL createFormActive and manipulate cell data accordingly with my didSelectRowAtIndexPath looking something like this:
if (indexPath.section == 0) {
self.createFormActive = !self.createFormActive;
[tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationAutomatic];
}
This works pretty well. So you could say I have found a solution, but feel free to use this thread as inspiration or discussion.
I've had good results with the "showing more cells if the button is pressed" approach. Relying on the table view's built-in animations is good bang for the buck IMO. Initially, however, I found managing a dynamic data model and calculating the batch updates can get out of hand quickly.
I ended up writing the TLIndexPathTools framework to make these things easy. Take a look at the Settings example project, a "settings" table view that morphs into a couple of different configurations as you make selections. I've done a much more elaborate real-world settings screen with table animation and it turned out very well.

Variable sized UITableViewCells

I have somewhat of an odd problem. I've already got a UITableView setup with custom heights for my table cells. The problem comes in when the content is loaded in the background for the cells. I need the cells to load asap, but theres a big image for each cell, that may take some time to load (from the network or flash), so the load is started in the background when the cell is first loaded. This means that the height for the cell is initially wrong, and needs to be updated later with a minimum of glitching. The problem I'm having is that it doesn't always want to update the table, or the image, and can cause some excess scrolling.
It's also incredibly slow. I have some optimizations I need to do to make it a bit less slow (make smaller copies of the main image, and cache the height for the cells temporarily).
I've tried all sorts of things. I make sure the table is told to update in the main thread, I've tried a bare set of beginUpdates/endUpdates, reloadRows/reloadSections (table is in grouped mode, with one row per section). I have yet to get the table to update properly once the image has finished loading.
If anyone has some insight into how to get this to work as smoothly as possible I'd appreciate it.
The place to go is your heightForRowAtIndex: method. It needs to know if the image exists or not and return a value accordingly. Every time each row finishes loading it's image, you should call reloadRows~ method like this:
NSArray *indexPaths = [NSArray arrayWithObject:[NSIndexPath indexPathForRow:x inSection:y]];
[self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
I'm not entirely sure if this will cause some heavy scrolling on your tableView, but it's an option.
Another option is to update in chunks. Say every 5-10 images (depending on how many you have) that get loaded reload their respective rows with that same method.

Middle animation in grouped table view looks horrible

I've found similar question, but there is no answer (sorry, answer just doesn't work).
So I have grouped table and I want to animate content update instead of doing [tableView reloadData].
I do that by using this piece of code:
// Data source already updated here, but reloadData wasn't called
[self.tableView beginUpdates];
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationMiddle];
[self.tableView endUpdates];
I uploaded 2 examples of the animation:
Plain: http://cl.ly/3u1M3l1w3V3J (slow motion)
Grouped: http://cl.ly/1O3Z2M280n0z (slow motion)
As you can see difference is huge.
I don't change my code at all, just change tableView style in the storyboard.
Does it men that there is no other way then subclassing UITableView and UITableViewCell and implement my very own animation using CoreAnimation?
Implementing your own animation with CoreAnimation shouldn't be necessary when it comes to animating the rows of the table.
UITableView supports much more advanced animations than simply reloading a section and I suggest that you take a look at them.
Since you are shuffling the rows in your videos you should take a look at moveRowAtIndexPath:toIndexPath: (on UITableView). You put the calls to it within beginUpdates and endUpdates.
By knowing the order before and after the re-shuffle you can move all the rows into their new places and have them slide into their correct place.
It will take some thinking to figure out where each row should go but it will be much easier than rolling your completely custom solution.

How To Ensure User Sees Row Insert Animation iOS

I've got an app that displays information from our web service. The user has the option to add rows to a table view to display more information. I'm using:
[self.tableView insertSections:indexSet withRowAnimation:UITableViewRowAnimationAutomatic];
to insert the row. It gives a nice bit of animation so the user knows where the row is they just inserted. My problem is that the row being added has to be in view in order for the animation to be seen. So I scroll down so that the row to be added is in view:
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
then start the ASIHTTPRequest asynchronously to get the info for the new row.
If there is a decent amount of data, the animation shows great. Problem is, if the user is on WiFi or not pulling much info, the ASIHTTPRequest returns before scroll completes and the new row is just there (the user doesn't see it get added). If I force a delay (.03 seconds) between the call to scroll the Table View and start the request, I get the desired effect, but then there is a longer delay than necessary on the loads that were already being displayed as desired.
I thought I would try to save the time, in ms, from when the view was scrolled and the request finished, and make sure it was longer that .03 seconds, but I kept getting an error with
[NSDate timeIntervalSinceReferenceDate]
so I figured I'd ask here if there was a better way before I hacked into that solution more.
Thanks!
Using a combination of timers and a couple flags will help you out, which is basically what you are already doing.

Graphical glitches when adding cells and scrolling with UITableView

I am using a UITableView to display the results of a series of calculations. When the user hits 'calculate', I add the latest result to the screen. When I add a new cell, the UITableViewCell object is added to an array (which is indexed by tableView:cellForRowAtIndexPath:), and then I use the following code to add this new row to what is displayed on the screen:
[thisView beginUpdates];
[thisView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation: UITableViewRowAnimationFade];
[thisView endUpdates];
This results in the new cell being displayed. However, I then want to immediately scroll the screen down so that the new cell is the lowermost cell on-screen. I use the following code:
[thisView scrollToRowAtIndexPath:newIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
This almost works great. However, the first time a cell is added and scrolled to, it appears onscreen only briefly before vanishing. The view scrolls down to the correct place, but the cell is not there. Scrolling the view by hand until this invisible new cell's position is offscreen, then back again, causes the cell to appear - after which it behaves normally. This only happens the first time a cell is added; subsequent cells don't have this problem. It also happens regardless of the combination of scrollToRowAtIndexPath and insertRowsAtIndexPath animation settings.
EDIT:
I've now started inserting cells at the second-to-last position of the table, rather than the end, and the problem still occurs - when first inserted, a cell is 'invisible' until it goes offscreen and comes back on again. What could be causing this, and how can I force the cell to be drawn as soon as it is added to the table?
You're having problems because your updating the table without updating the data model backing it. Tables don't actually know how many rows they have nor what cells to display. They depend on the datasource and the delegate to tell them these things. Your design expects the table itself to track them.
insertRowsAtIndexPaths: is intended to be used for moving existing rows around a table, not for adding entirely new logical rows. When you insert an entirely new cell, the tableview looses track of how many rows it actually has.
Before you display a new row, the first thing you should do is update the values returned by:
– numberOfSectionsInTableView:
– tableView:numberOfRowsInSection:
... to reflect the addition of the new rows. This will allow the table to understand how big it is.
Then you need to update cellForRowAtIndexPath: to return the correct cell for the added row. Then you need to reload the table.
After you've done that, you should be able to scroll the tableview to the end and have the cell display properly.
The important thing to remember about tables is that they are dumb. The table itself holds no data, doesn't know how many sections and rows it has or what order the rows and sections come in. All the logic about data, sections, rows, cells and cell contents comes from the datasource and/or the delegate. When you want to change a table, you actually change the datasource and/or the delegate and then the table will reflect those changes automatically.
Edit:
Upon rereading the parent, I see that your putting the actual UITableViewCell objects in your data array and that you have one cell for each row.
This is not how tableviews are supposed to work and this will not scale beyond a few dozen rows at most.
Tableviews are intended to be an illusion that allows you display a lOGICAL table which has an arbitrary high number or rows. To that end, it only keeps enough UITableViewCell objects alive to cover the visually displayed area in the UI. With a default cell height of 44 pixels this means a tableview will never have more than 9 cell objects at a time.
Instead of eating memory holding cells that are not displayed, the tableview lets the delegate dequeue a cell that has scrolled off screen, repopulate it with the data of another LOGICAL row and then display it in a new position. This is done in cellForRowAtIndexPath:
You really need to start over here with your design. Your data needs to be kept separate from the user interface objects. You don't want to have more cells alive at anyone time than absolutely necessary because your memory use will balloon and your response time will degrade. Your current problem is the result of this unusual design.
When you've done that, you can add the result row as outlined above.
Try to scroll with some time shift after cell update via NSTimer or performSelector:withDelay:. It can help but to fix all problems I think there need to do more work.
The glitches may be caused because a UITableView considers itself the owner of any UITableViewCell instances it is displaying, and reuses them as needed. Part of that process is calling prepareForReuse on the cell. Since you are keeping the cells in an array, you do not want them reused. Try implementing an empty prepareForReuse in your UITableViewCell class. Or just create cells dynamically in tableView:cellForRowAtIndexPath: as apple recommends.
I used what Skie suggested to avoid the problem in the following way:
Immediately after adding the row:
[self performSelector:#selector(scrollToDesiredArea:) withObject:newIndexPath afterDelay:0.4f];
This called the following:
-(void)scrollToDesiredArea:(NSIndexPath *)indexPath{
UITableView *thisView = (UITableView*)self.view;
[thisView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}
The delay of 0.4s seems to be sufficient to avoid the glitching; any less and it still happens. It may have to be different on varying models of iPhone hardware, though - I only tested on emulator.

Resources