In Mail, if you manually refresh a mailbox via pull to refresh, if you don't have any new emails, the view slides back up to hide the spinner but the appearance of existing cells don't change - they don't animate at all. But if you did have new emails, those animate in from the top. If you had no emails listed before the refresh, all the cells animate in quite nicely. Also, if you refresh and an existing email had been deleted, its disappearance is animated but none of the other cells animate besides the entire table moving up to take the place of the removed cell. I would like to achieve this same behavior in my app.
Currently, when my data is fetched for the first time, it immediately appears in the table without any animation. If some data is deleted and the user manually refreshes, the entire table is instantly updated so there is no animation of the cell disappearing, it's just instantly replaced with the row underneath it. The same behavior occurs if cells were already displayed and refreshing results in new data being added to the table - it just instantly appears.
How can I implement animation for appearance and disappearance of cells when I reloadData? But not animate existing cells if they don't change.
My setup is to fetch the data, parse the JSON into a data structure (array or dictionary), then call reloadData, and then cellForRowAtIndexPath gets the data from the structure. When the user refreshes, it performs those same steps again.
My attempts so far haven't been successful. I tried instead of simply calling reloadData, I call [self.tableView reloadSections:[NSIndexSet indexsetWithIndex:0] withRowAnimation:UITableViewRowAnimationTop]; but this only animates the first section and it always animates it, even if none of the data has changed. My other attempt also always animates it:
[self.tableView beginUpdates];
[self.tableView deleteSections:[NSIndexSet indexsetWithIndex:0] withRowAnimation:YES];
[self.tableView insertSections:[NSIndexSet indexsetWithIndex:0] withRowAnimation:YES];
[self.tableView endUpdates];
When inserting the rows, keep the index paths being inserted in an array, then:
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:arrayOfInsertedIndexPaths withRowAnimation: UITableViewRowAnimationTop];
[self.tableView endUpdates];
I haven't confirmed directly, but I'm pretty sure this will behave as you wish when now insertions are made.
EDIT - Row-accurate animation is going to require row-accurate knowledge about the change in the model. How to get the index paths being inserted depends on how you're getting the data. In the toughest scenario, you're just getting a new collection and will need to work out for yourself what has changed. Your model objects probably need to implement equivalence** so you can conclude that different instances should be treated as the same.
** as in NSString isEqualToString: which is an equivalence test, not an equality test as it's name might suggest.
We're comparing the objects themselves, not row indexes, so knowing about deletions is hard, but no harder than knowing about insertions. As an example, here's a category I created for NSMutableArray that handles only insertions (it's ideal for an email or other timeline sort of app that's expecting new stuff, not deletions).
#import "NSMutableArray+Inserts.h"
#implementation NSMutableArray (NSMutableArray_Inserts)
// insert elements of array into the receiver, sorting on a key
// answer an array of index paths where the insertions happened
- (NSArray *)insertFromArray:(NSArray *)array sortedOn:(NSString *)key ascending:(BOOL)ascending {
// first insert everything from array (preserving uniqueness according to shallow equality)
for (id newElement in array) {
NSInteger position = [self indexOfObject:newElement];
if (position == NSNotFound) [self addObject:newElement];
}
// now sort using key
[self sortUsingDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:key ascending:ascending]]];
// now find the insertions using deep equality
NSMutableArray *answer = [NSMutableArray array];
for (id newElement in array) {
NSInteger position = [self indexOfObject:newElement];
id element = [self objectAtIndex:position];
if (element == newElement) {
[answer addObject:[NSIndexPath indexPathForRow:position inSection:0]];
}
}
return [NSArray arrayWithArray:answer];
}
A couple of notes: 1) I lazily hardcoded the index path section to 0. If your model has multiple sections, you'll want to call this for each and add a section parameter so you get good index paths back, 2) the sorted insert feature of this is pertinent to my particular app, you might not need it, and can just remove those two params and delete the self sortUsing... line. 3) one way to account for deletions would be to add an analogous method that answers the index paths of elements in the receiver NOT in a passed array.
Related
Say, I have an entity "Parent" which can have multiple "Child". During deletion, I do this:
[self.parent removeChildObject:self.childToRemove];
[self.managedObjectContext save:nil];
With only that, the scene does not change and the row is not removed. So what I did is I retrieve again all "Child" under that "Parent" and call
[self.tableview reloadData];
While it did it correctly, however, the deletion seems instantaneous. The row just disappear in a blink of an eye. I want it to animate with row fading.
I read that only the entity which was fetched using NSFetchedResultsController calls this delegate method "didChangeObject" where I put my row animation fade. However, for retrieving the "Child", I didn't fetched it using the controller, but I fetched it from
[self.parent children];
This bit of code should do it.
[self.tableView beginUpdates];
NSArray * rowIndexPathArray = #[childObjectIndexPath];
[self.tableView deleteRowsAtIndexPaths:rowIndexPathArray withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
I have spent hours searching for the solution with out any luck. I am trying to delete a row (also deselect same row) programmatically. After row deletion call below, UITableViewDelgate methods get called expectedly and data source is updated but UITableView is not refreshed. deselectRowAtIndexPath call also does not work. I tried all kinds of scenarios as shown by commented lines.
Here is my code:
checkoutPerson is called as a result of observer listening for NSNotificationCenter messages.
- (void) checkoutPerson: (NSNumber*) personId {
Person *person = [_people objectForKey:personId];
if( person )
{
// Remove person from data source
int rowIndex = person.rowIndex;
S2Log(#"Deleting row number=%d", rowIndex);
[_allKeys removeObjectAtIndex:rowIndex];
[_people removeObjectForKey: personId];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:0];
//[[self tableView] beginUpdates];
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
S2Log(#"Deleting indexPath row=%d", [indexPath row]);
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
//[[self tableView] endUpdates];
S2Log(#"Reloading data");
//[[self tableView] reloadData];
//[self performSelector:#selector(refreshView) withObject:nil afterDelay:1.5];
//[self.tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:YES];
}
}
I will appreciate for help.
Thanks
-Virendra
I believe deleted cell is not being recycled. If I delete row in the middle, last row is always erased (since there is one less item) but the deleted row remains.
Use the above code between two function for table view
[tableView beginUpdates];
// the deletion code from data source and UITableView
[tableView endUpdates];
By calling this functions you are telling UITableView that you are about to make updates for deleting your cell.
Edit
The other problem I see with your code is you first delete the data from the data source.
Now you are asking for the UITableViewCell (which actually reloads the UITableView)
and then you are deleting the row from UITableView
I guess you should fetch the UITableViewCell before deleting values from your data source.
I found the problem. It has nothing to do with the code I posted above. It is syncing problem between visual display and the contents of data source. I have an embedded UITableView as part of a composite view. In composite view's controller, I was wiring up UITableView's delegate and data source to an instance of UITableViewController. Instead of this, I should have set UITableViewController's tableView property to the embedded UITableView. It seems that UITableView has to be contained within UITableViewController in order to correctly sync up table view visual display to the contents of data source. This also fixes row deselection and scrolling. I also needed to delay reloadData call in which case deleteRowsAtIndexPaths:withRowAnimation is not required. All you need is to modify the contents of your data source and call reloadData with a delay of 1.5 Seconds.
Thanks to all for great help.
I have been struggling with this for a week and my head is about to explode.
Basically I use Prototype Cell, in CellWillAppear I did a little customizations like background color. Nothing fancy.
Due to this, my table view is always empty at start up (no cell) unless the array (data source) is filled with something. So what I did was in NumberOfRowsInSection:
return dataArray.count < 10? 10 : dataArray.count
I am doing this because I would like to see at least some empty cells when there is no data.
Meaning it will show on start up at least 10 empty cells.
To add data to the cell, I call the delegate method in my tableviewcontroller each and every time to add one single entity in the data array (am doing this, because I think it would be faster than waiting until the whole array is filled then call [self.tableView reloadData];) and then refresh it by using reloadRowsAtIndexPaths. But it crashed every single time when it reached to index 10 (error: ... before update number of data was 10, but after update is 11).
What I really want is:
1.) prepare some data
2.) send it to uitableview controller and add it to an array there, instead of waiting and then sending a whole array to table view and refresh at once.
3.) reload just one row after the update (instead of using reloadData -> since I have different color of cell, the whole reload thing cause my table view flash madly).
The one thing I am doing to cell customization is in willDisplayCell:
What I did there is to change the background color of the cell. Again, nothing fancy.
But since there is no data at start up, no cell is ever visible (ui tablew with no cell at displayed at all), unless I did this
return dataArray.count < 10? 10 : dataArray.count;
just so there are at least 10 empty cells showing (WHY do I have to do the above just to display some customized empty cells beats me...).
Using reloadData is to refresh no problem, but since I am updating the data source array in table view every time data is ready instead of saving all prepared data to this array and send it over to table view to update by using reloadData, I would like to update row by row.
I kind of feel that the error comes from the fact that, if I add one item in the array and then call reloadRowsAtIndexPath, it will say "Ok, you had one item before, but after update there is 2! Inconsistency.."
I have already tried using [tableView beginUpdate]; and [tableView endUpdate];
Nothing has worked so far.....
So to sum up: how can I have different colors of cells showing even when the data array is empty on start up (just like the default ui table view with cells displaying completely even with no data) and update just one of the cells once a piece of data is ready instead of updating the whole ui table view with reloadData?
Many thanks in advance, please advise. Regards.
"how can I have different colors of cells showing even when the data array is empty"
Don't have an empty array, have a mutable array where all the members are initially empty strings, and replace those with your real data when you get it.
"update just one of the cells once a piece of data is ready"
Update your array with the new data, and then use reloadRowsAtIndexPaths:withRowAnimation: to update the table. If you want to see the table update row by row (slow enough to see), then put your data in a temporary array first, then add it one element at a time using performSelector:withObject:afterDelay:, calling reloadRowsAtIndexPaths:withRowAnimation: after each addition.
It's a little hard to tell exactly what you want, but here is an example of what I mean. This table displays 20 empty rows, all with different colors, for 2 seconds, then it replaces the empty strings in displayData with the strings in theData one by one at a rate of 10 per second.
#interface TableController ()
#property (strong,nonatomic) NSArray *theData;
#property (strong,nonatomic) NSMutableArray *displayData;
#end
#implementation TableController
- (void)viewDidLoad {
[super viewDidLoad];
self.displayData = [#[#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#""] mutableCopy];
self.theData = #[#"One",#"Two",#"Three",#"Four",#"Five",#"Six",#"Seven",#"Eight",#"Nine",#"ten",#"Black",#"Brown",#"Red",#"Orange",#"Yellow",#"Green",#"Blue",#"Violet",#"Gray",#"White"];
[self.tableView reloadData];
[self performSelector:#selector(addData) withObject:nil afterDelay:2];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.displayData.count;
}
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
UIColor *cellTint = [UIColor colorWithHue:indexPath.row * .05 saturation:1.0 brightness:1.0 alpha:1.0];
cell.backgroundColor = cellTint;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.textLabel.text = self.displayData[indexPath.row];
return cell;
}
-(void)addData {
static int i = 0;
[self.displayData replaceObjectAtIndex:i withObject:self.theData[i]];
[self.tableView reloadRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
i++;
if (i < self.displayData.count) [self performSelector:#selector(addData) withObject:nil afterDelay:.1];
}
If you don't want any delay between row updates, and you want to make it work when displayArray has a different number of rows that theData, this version of addData should work:
-(void)addData {
static int i = 0;
if (i < self.displayData.count && i< self.theData.count) {
[self.displayData replaceObjectAtIndex:i withObject:self.theData[i]];
[self.tableView reloadRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
i++;
[self addData];
}else if (i >= self.displayData.count && i< self.theData.count) {
[self.displayData addObject:self.theData[i]];
[self.tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
i++;
[self addData];
}
}
I am new in iPhone and now I am struggling to create and inserting object.
I explain first
Here I have one tableview with some cell value which is getting from my array whose name is appDelegate.array1
now I can change some value in table view cell and now after that I want to insert this new cell value in same array appDelegate.array1. So how can I do this.
I tried to do it like this.
-(void)viewWillAppear:(BOOL)animated
{
[appDelegate.array1 addObject:indexPath];
[tableView reloadData];
}
it is correct or not if yes then why my application will terminate and if not then please give me correct method to add object in array
Note this points.
1) Check if your array is Mutable array...Then only you can add new items to array at run time
2) What is indexPath in your code, which is bit confusing..
3) To add object you use these (or more)functions
[yourArray addObject:your_object];
Or to insert into the array you can use
[yourArray insertObject:your_object atIndex:your_index];
Hope this help
Instead of reloading entire table view for adding just one item, Just add the element to the array and update the table view....
-(void) addObject : (id) inObject
{
if(inObject)
{
[appDelegate.array1 addObject:inObject];
[mTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
}
}
How are you declear your array?
you need to make it a NSMutableArray.
NSMutableArray *array1;
Your method is right but there can be number of problems in ur logic.
Like you add indexPath. (what is in indexPath?) is it nil or something like that.
your appDelegate object correctly initialized or not.
There is certainly a problem in your logic.
I'm working on a feed reader iOS project. Feed entries are listed in a UITableView in reverse chronological order. On launch, they're loaded from a database into an array.
When the app syncs to feeds, it creates a new array for the new order of things, and then to update the table, compares the new array to the old array to determine what cells to delete, update, or insert. The way I've done it is naïve, and therefor really inefficient: Lots of calls to indexOfObject: to see if an item in one array is in the other array. Twice. once for each new entry as it's added to the new array, to see if it's in the old array, and then once for each entry in the old array, to see if it's not in the new array.
As a database professional, this design offends me.
But it must be a pretty common pattern. What would be the most appropriate, sufficiently Cocoa-ish way to go about this?
Turns out I was coming at this wrong. The solution I found was to add and remove items from the array as usual, and then to call insertRowsAtIndexPaths:withRowAnimation:, reloadRowsAtIndexPaths:withRowAnimation:, and deleteRowsAtIndexPaths:withRowAnimation: as appropriate for each row added, changed, or moved. The fault in my previous plan was the thought that I should wait until all the changes and been made, and then call each of those methods only once in a beginUpdates/endUpdates block. Turns out the block wasn't actually necessary, as the modification methods can be called outside of them.
It was much easier to call each method once for each cell inserted, updated, or deleted, than to calculate all the changes at the end and commit them at once. Was just too confusing, error-prone, and inefficient to try to do it all at once.
So the code I ended up with looks like this:
if (parsedItem.savedState == ItemModelSavedStateInserted) {
// It's a new entry. Insert it.
[items addObject:parsedItem];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:items.count - 1 inSection:0]] withRowAnimation:UITableViewRowAnimationTop];
} else {
// It's an existing entry. Find it in the portal and move it, if necessary.
NSUInteger foundAt = [items
indexOfObject:parsedItem
inRange:NSMakeRange(currentItemIndex, items.count - currentItemIndex - 1)
];
if (foundAt == currentItemIndex) {
// It hasn't moved!
if (parsedItem.savedState == ItemModelSavedStateUpdated) {
// It was updated, so replace it.
[items replaceObjectAtIndex:currentItemIndex withObject:parsedItem];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:currentItemIndex inSection:0]] withRowAnimation:UITableViewRowAnimationMiddle];
}
} else {
// It has shifted position.
if (foundAt != NSNotFound) {
// It has moved.
[items removeObjectAtIndex:foundAt];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:foundAt inSection:0]] withRowAnimation:UITableViewRowAnimationBottom];
}
// Need to insert it.
[items insertObject:parsedItem atIndex:currentItemIndex];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:currentItemIndex inSection:0]] withRowAnimation:UITableViewRowAnimationTop];
}
}
Consider using NSSets for differencing the set of current items and the set of new items, with a single NSMutableArray to hold the ordered current list. You would probably want to remove each of the expired items from the array, then insort each of the unexpired new items into the array. The items that you needed neither to remove nor to insort are the items that you may want to update.