Dynamically updating sections of UITableView without using reloadData - objective-c

First off I sincerely apologize if this has been answered. I have been battling with this for too long. I would appreciate any kind of help.
I am developing an application that gets data asynchronously from different web sources and parses it into objects that are handled by my main Model. This model provides information for a UITableView. When data is received and parsed, i need to modify the number of sections and rows in the table view. I first receive the data that decides the number of sections, and then asynchronously data that modifies the number of rows in each section and their content. I have the table views data sources set up.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return [self.sijainti.pysakit count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
//if ([[[self.sijainti.pysakit objectAtIndex:section] ajatStr] count] == 0) return 1;
NSLog(#"ajatStr of section: %d count: %d",section, [[[self.sijainti.pysakit objectAtIndex:section] ajatStr] count]);
return [[[self.sijainti.pysakit objectAtIndex:section] ajatStr] count];
}
The object names are in Finnish (long story). The second line of numberOfRowsInSection was commented to ease debugging. Now, because the data is dynamic i need to update the interface when i have data ready for display, or the user wont know about it until they scroll away and back again. I have a method for this:
- (void)updateRowWithCell:(SCPysakki *)valmisPysakki {
if (!valmisPysakki.isInArray) return;
int section = [self.sijainti.pysakit indexOfObject:valmisPysakki];
[self.tableView beginUpdates];
NSIndexSet *sectionSet = [NSIndexSet indexSetWithIndex:section];
[self.tableView reloadSections:sectionSet withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
}
This is called only after the number of sections is known. I do NOT want to use reloadData due to efficiency, and animation. I used to have this set up so that i had only rows and was just drawing rows inside of the rows. But once the rows became taller than the iphone screen, it became very unclear, because you couldn't see the title. So i made them sections, and the rows drawn real rows. Then i was suddenly faced with a problem. reloadRowsAtIndexPaths: withRowAnimation: worked absolutely fine but using reloadSections: withRowAnimation: gave me this exception:
* Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-1914.84/UITableView.m:1037
2012-09-17 04:11:27.769 pysappi[2351:f803] * Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 1. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (0), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
I was able to eliminate this by adding the [self.tableView endUpdate] and [self.tableView endUpdate] to the update method. But oh no, that only works if I:
Use reloadData after receiving the data deciding the number of sections (which in turn shortens the tableview so that after the whole update the user at the top of the table after every single update. I would rather update the sections dynamically, without using reloadData, except when removing Sections, but if I try to do this i get the exception again.)
update the sections after receiving data for only one Row (so if I try to update all the rows in the section once I've received all their data, I get the exception again.)
Now this creates a situation where creating a good user interface becomes impossible. I'm convinced that the underlying problem is not solved with the start and end updating calls, but that just fixes it for some very very odd reason.
I also tried adding the rows manually with insertRowsAtIndexPaths: and updating them manually also, but this returns the exact same error. During a long debugging progress i have been able to determine that this exception rises after cellForRowAtIndexPath: gets called and it returns a cell for one of the new rows that was added during the updating, even though the numberOfRowsInSection should have updated the amount of rows. I also suppose that if those few circumstances apply this is why the start and end updating methods make the row updating work.
The only thing that has worked outside of what i explained above, is using only reloadData, but that is just practically impossible considering the amount of updating.
This became a lot longer than i thought, and its 4.30 AM. Not the first time I'm up this late thanks to this exception. If you need more information ask. Thank you in advance.

(Answered in the comments. Converted to a community wiki answer. See Question with no answers, but issue solved in the comments (or extended in chat) )
#deleted_user wrote:
number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (0), - you had a section with zero rows and did an update with a row - cocoa didnt see that as an insert and failed you

Related

OSX NSTableView insertRowAtIndexes

i already checked Using NSTableView insertRowsAtIndexes solution but It does not solve my problem.
i want insert row in nstableview at particular index(add dynamically)
index Set is valid, still it causes Program crash
NSIndexSet *indexSet=[NSIndexSet indexSetWithIndex:i];
[myTableView insertRowsAtIndexes:indexSet withAnimation:NSTableViewAnimationEffectFade];
1)is any thing wrong in my code?
2)Is there any another way to add row at particular index(add dynamically)?
The code is correct but first you have to insert the model object in the data source array to keep model and view in sync.
I got My mistake..... problem In other Code so This code is fine
But I want to add some points About insertRowsAtIndexes: method
Hope it will helps to other people
1)Dont called reloadData() because you are adding particular number of rows so calling reloadData() will reload all data and it will causes crash
2) Calling this method multiple times within the same beginUpdates and endUpdates block is allowed, and changes are processed incrementally
3)Most Important thing is indexSet must be within range
if you are Enter valid indexSet then The numberOfRows in the table view is automatically increased by the count of indexes.
4)you can select animation according to your need
Sample Code :
[yourTableView beginUpdates];
NSIndexSet* theIndexSet = [NSIndexSet indexSetWithIndex:[self.yourTableContents count]-1];
[yourTableView insertRowsAtIndexes:theIndexSet withAnimation:NSTableViewAnimationEffectFade];
[yourTableView endUpdates];

Amount of rows in UITableview with sections

i got a UITableview with sections.
The sections are handled by Core Data's NSFetchResultsController, however this is less important concerning my problem.
I'd like to have a predefined amount of rows in my UITableview. Let say i start the application, i'd like to see only 10 rows, even if i got 14 entries in Core Data. When i click a button "load more.." the next 10 rows should be loaded (loading is implemented i just need to figure out a way to display the right amount of rows). If i got 10 rows at the beginning and in the first section are 7 and in the second are 5 then the last two rows of the second section should not be display until i click the load more button to show the next 10. So i think i have to implement this logic in numberOfRowsInSection and numberOfSectionsInTableView but i need a hint from you how to solve this...
Basically i would need a loop which loops through the sections. If the amount of rows i want to display is reached i return all sections until this point to be displayed. Then i would need to retrieve all the rows of this sections and the rows of the sections before to calculate the numberOfRowsInSection for each section...
Anyway somebody who can help me implement this ?
Thanks in advance
Perhaps a good way to solve this is to use the fetched results controller. You can try setting the fetchLimit on the fetched results controller's fetchRequest. If the sorting of your FRC is correct, it should retrieve the first n objects.
-(void)loadRows:(NSUInteger)numberOfRows {
NSFetchRequest *request = self.fetchedResultsController.fetchRequest;
request.fetchLimit = numberOfRows;
NSError *error;
[self.fetchedResultsController executeFetchRequest:request error:&error];
}

Using for loop to add to an array, objects aren't being added until for loop is completely done looping

So, I am using the code below to cycle through json arrays if the json array returns positive, I add the json array to another array containing all of total arrays. This can take some time to complete however, because it is going through several different json urls plugging in different dynamic information.
I edited the code below to show more basic information. My problem is that data isnt actually entered into the array until the for loop is finished. I need it to be added as soon as it finds it, not after it runs through all the onlineChannels.count - Any help?
for (int i = 0; i<onlineChannels.count; i++) {
[jsonResults addObject:[parsingJson objectAtIndex:0]];
NSLog(#"%i",jsonResults.count);
[MBProgressHUD hideAllHUDsForView:self.view animated:YES];
[self.tableView reloadData];
} else {
[MBProgressHUD hideAllHUDsForView:self.view animated:YES];
[self.tableView reloadData];
}
}
My problem is that data isnt actually entered into the array until the for loop is finished.
How do you know?
I see that you're calling -reloadData, so perhaps you're expecting the change to be reflected in your table immediately? I believe that your objects are being added to the array immediately, but the change isn't displayed on the table right away. -reloadData doesn't actually redraw the table -- drawing happens as part of the main run loop. You're probably running this code on the main thread, and by doing so you're preventing the run loop from getting any time to redraw the table.
The best way to solve this sort of problem is to move time consuming processes like your loop to background threads. That will leave the main thread free to redraw the table as necessary, and it'll make your app more responsive as well.
Also, making the table reload all its data every time you add a single item even when that item's row isn't being displayed seems quite wasteful. Consider instead calling a method like reloadRowsAtIndexPaths:withRowAnimation: to update only the part of the table that has changed.

Using NSTableView for logging?

I've been trying to work out how to use table views and I'm a little stuck if I'm honest. I wanted to use a tableview with a limited number of rows (say 50 max). It starts of empty, with 0 rows. Then I wanted to do something along the lines of:
[self logMessage:#"Waiting for response"];
Which inserts a new row at the bottom with the above text. If I do another call to this pseudo function:
[self logMessage:#"Server response received"];
It should insert yet another new row below the previous row, and ensure it is visible. Once the above limit of 50 is reached, and a new message is inserted, I wanted the oldest message to be removed. All of this would be scrollable, with the latest being visible by default.
Am I looking at the right thing to do this? Eventually, I was hoping to have this in a nice little drawer below the main window, which I can then toggle from the main menu if needed. But as I said, I can't work out how to use a table view properly, it doesn't seem to be as straight forward as other objects are.
Any example code would be greatly appreciated!
Since log viewer is a read-only application of a UITableView, the way you do it is rather straightforward once you understand the basics. Recall that table views rely on their data models to provide them with the correct information that needs to be displayed.
A data model for "the last fifty lines of log" could be as simple as an NSMutableArray: use insertObject:atIndex: to add lines, and removeLastObject to remove the "overflow" lines, like this:
NSMutableArray *logLines = [NSMutableArray array]; // <<== this goes into the init method
-(void) addLogLine:(NSString*)line {
[logLines insertObject:line atIndex:0];
while (logLines.count > 50) {
[logLines removeLastObject];
}
}
Now you can use logLines as your table's "model": the data provider can tell how many lines there are by looking at logLines.count; the content of each row in the table will be the object at the corresponding index in logLines, and so on. Take a look at the UITableView section of your favorite iOs tutorial for the "boilerplate code" that needs to be written in order to display array elements in a UITableView.

How do I efficiently update a UITableView with Animation?

My iPad app features a UITableView populated from a feed. Like most RSS readers, it displays a list of links to blog posts in reverse chronological order, with their titles and a summary of each post. The feed updates frequently, and is quite large, around 500 posts. I'm using libxml2 push parsing to efficiently download and parse the feed in an NSOperation subclass, constructing entry objects and updating a database as I go. But then I need to update the UITableView with changes.
So far, the app has been updating the UITableView for every post parsed, as it is parsed. The parser performs a selector on the main thread to do this work. But this leads to some serious lag for a couple of seconds if a lot of cells need to be updated. I can mitigate this by running the table update on a background thread, but it seems that this is not a good idea. So now I'm trying to figure out how to update the table more efficiently on the main thread.
I could just call reloadData when all the posts have been parsed, but it's not very user friendly: there's no animation to indicate that anything has changed, just a flash and the new data is there. I'd much rather have it animate to show that new posts are added and old posts removed. Existing posts that are not removed from the feed should be pushed down the table by the new posts appearing at the top.
I know this is possible. Byline, to give one example, does a beautiful job. Each post is added or removed from the UITableView one-at-a-time with no gaps showing the table background. All without making the UI in the least bit unresponsive. How is that done??
My latest attempt is to update the table only after all the posts have been parsed (the parser is quite fast, so it's not much of a delay). It then loads the existing posts in an NSDictionary mapping their IDs to their indexes in the array used as the table data source. It then iterates over every object in the newly-parsed array of posts, adding NSIndexPath for each to arrays that are later passed to -insertRowsAtIndexPaths:withRowAnimation:, -deleteRowsAtIndexPaths:withRowAnimation:, and -reloadRowsAtIndexPaths:withRowAnimation: as appropriate to insert, remove, move, or update cells. For 500 posts, this takes around 4 seconds to update, with the UI completely unresponsive. That time is used almost exclusively for the UITableView animated updates; iterating over the two arrays of posts takes very little time.
I then modified it so that those are updated without animation, and I have separate arrays to insert/delete/reload with animation only for row positions corresponding to currently-visible rows. This is better, but gaps appear as posts are removed and new ones added.
Sorry this is so long-winded, but here's the upshot:
How can I update a UITableView, with new cells pushed on, others pushed off, and still others moved from one position to another, with up to 500 cells in the UITableView (6-8 are visible at one time), and each animation happening in sequence, all while the UI remains completely responsive?
This question actually has three answers. That is, there are three parts to this question:
How to keep the UI responsive
How to keep the updating fast
How to make table update animation smooth
UI Responsiveness
To solve the first problem, I now make sure that no more than one table-updating message can be delivered on each iteration of the main event loop. That prevents the main thread from locking up if the background thread is feeding it stuff to do faster than it can cope with it.
This is done thanks to example code sent to me by Byline author Milo Bird, which I then integrated into Dave Dribin's DDInvocationGrabber. This interface makes it super easy to queue a method to be invoked on the next available iteration of the main event loop:
[[(id)delegate queueOnMainThread]
parserParsedEntries:parsedEntries
inPortal:parsedPortal];
I quite like how easy it is to use this method. The parser now uses it to call all of the delegate methods, most of which update the UI. I've released this code on GitHub.
Performance
As for performance, I was originally updating one UITableView row at a time. This was effective, but somewhat inefficient. I went back and studied the XMLPerformance example, where I noticed that the parser was waiting until it had collected 10 items before dispatching to the main thread to update the table. This was key to keeping the performance up without making the UI lock up by updating all 500 rows at once. I played around with updating 1, 10, and all 500 rows in a single call, and updating 10 seemed to offer the best tradeoff between performance and UI lockup. five would probably work pretty well, too.
Animation
And finally, there's the animation. Watching the “Mastering Table Views” WWDC 2010 session, I realized that my use of the deleteRowsAtIndexPaths:withRowAnimation: and updateRowsAtIndexPaths:withRowAnimation: methods was wrong. I had been keeping track of where things should be added and removed in the table and adjusting the indexes as appropriate, but it turns out that's not necessary. Inside a table update block, one only needs to reference the index of a row from before the update, regardless of how many may be inserted or deleted to change its position. The update block, apparently, does all that bookkeeping for you. (Go to about the 8:45 mark in the video for the key example).
Thus, the delegate method that updates the table for the number of entries passed to it by the parser (currently 10-at-a-time) now explicitly tracks the positions of rows to be updated or deleted from before the update block, like so:
NSMutableDictionary *oldIndexFor = [NSMutableDictionary dictionaryWithCapacity:posts.count];
int i = 0;
for (PostModel *e in posts) {
[oldIndexFor setObject:[NSNumber numberWithInt:i++] forKey:e.ident];
}
NSMutableArray *insertPaths = [NSMutableArray array];
NSMutableArray *deletePaths = [NSMutableArray array];
NSMutableArray *reloadPaths = [NSMutableArray array];
BOOL modified = NO;
for (PostModel *entry in entries) {
NSNumber *num = [oldIndexFor objectForKey:entry.ident];
NSIndexPath *path = [NSIndexPath indexPathForRow:currentPostIndex inSection:0];
if (num == nil) {
modified = YES;
[insertPaths addObject:path];
[posts insertObject:entry atIndex:currentPostIndex];
} else {
// Find its current position in the array.
NSUInteger foundAt = [posts indexOfObject:entry];
if (foundAt == currentPostIndex) {
// Reload it if it has changed.
if (entry.savedState != PostModelSavedStateUnmodified) {
modified = YES;
[posts replaceObjectAtIndex:foundAt withObject:entry];
[reloadPaths addObject:[NSIndexPath indexPathForRow:num.intValue inSection:0]];
}
} else {
// Move it.
modified = YES;
[posts removeObjectAtIndex:foundAt];
[posts insertObject:entry atIndex:currentPostIndex];
[insertPaths addObject:path];
[deletePaths addObject:[NSIndexPath indexPathForRow:num.intValue inSection:0]];
}
}
currentPostIndex++;
}
if (modified) {
[tableView beginUpdates];
[tableView insertRowsAtIndexPaths:insertPaths withRowAnimation:UITableViewRowAnimationTop];
[tableView deleteRowsAtIndexPaths:deletePaths withRowAnimation:UITableViewRowAnimationBottom];
[tableView reloadRowsAtIndexPaths:reloadPaths withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
}
Comments welcome. It's entirely possible that there are more efficient ways to do this (the use of -[NSArray indexOfObject:] is particularly suspicious to me), and that I may have missed some other subtlety.
But even so, this is a huge improvement for my app. The UI now stays (mostly) responsive during a sync, the sync is fast, and the table update animation looks just about right.
Have you tried [tableView beginUpdates]; and [tableView endUpdate];?