iOS 5 finally added a moveRowAtIndexPath:toIndexPath: method to UITableView. Problem is, calling it only moves the row, it doesn't update/redraws it. The only way to do so seems to be by finding the cell and calling setNeedsDisplay directly. But this is tricky since you never know whether the cell already got moved or not.
I can't call reloadRowsAtIndexPaths: either, since I'm not allowed to have 2 animations applied to a single row inside the same beginUpdates-endUpdates block.
There are may workarounds I can think of, like splitting the whole thing into 2 animation blocks. But I'm wondering whether I'm missing something, as the whole behavior seems odd to me.
Finally received an official answer from Apple regarding this issue:
Engineering has determined that this issue behaves as intended based
on the following information:
Only insert and update cause the cell to be requested from the data
source (the first because it's new and the second because you've
explicitly told the table view to update the cell). Delete and move
don't update the contents because that's not implicit in the action
being performed. If you need to change the contents of the cell
during the update, just reach into the cell and change the contents.
They like making it difficult for us don't they ;-)
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
// old API
//[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
//[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
// new API
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:newIndexPath]; // note use of newIndexPath
[tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath]; // NS_AVAILABLE_IOS(5_0);
break;
Note in the ChangeMove, configureCell has different params compared to its use in ChangeUpdate so be careful if copy pasting. Rather tricky to get that right.
Related
I am updating my UITableView with the following code:
int index = [self.guests indexOfObject:_guests];
[[self tableView] beginUpdates];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:index inSection:0]] withRowAnimation:UITableViewRowAnimationLeft];
[[self tableView] endUpdates];
But after calling [[self tableView] endUpdates], its not calling its datasource methods. In case of adding row, it calls its datasource methods. I think in case of deletion it does not need to ask anything to its datasource, but in case of adding a row it needs to ask its datasource about pretty much everything about the new row added like cellForRow, height etc etc. I just want to make sure that is it right if deleteRowsAtIndexPaths is not calling any of its datasource methods??
deleteRowsAtIndexPaths:indexPaths doesn't call delegate methods in all situation. It just simply deletes rows, If the rows being deleted are so many that they are occupying screen area, then it may call your delegate method. You have to explicitly call reloadData so that it refreshes its rows.
But calling reloadData immediately will spoil your animation or produce weird errors, since its rows are being deleted and you call reload method. (It may go crazy). The alternate solution is to call reloadData after a slight delay like 0.2 or greater like this:
[self.tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationLeft];
[self.tableView performSelector:#selector(reloadData) withObject:nil afterDelay:0.2]; //calling reloadData after a short delay. 0.2 or whatever suits you.
Don't forget about deleting data from your datasource's array or whatever you are using to hold data.
The answer is a question " why should it call all the datasource and delegate methods if the view is already popuated and all cells are readily available"?
well it Should be the behavior
I want to put new values in a table view.
My tableView:cellForRowAtIndexPath: has the following
if (cell == nil) { // do something }
When I remove the if statement on top and use reloadData the app crashes. Do I need to clear the old values in the table before I use reloadData? If so how?
Thanks
Edit:
I'm sorry i didnt mean that i put reloadData inside tableView:cellForRowAtIndexPath:. i put it inside another method and removed if (cell == nil) { // do something } from inside tableView:cellForRowAtIndexPath:
You should read the TableView Programming Guide to familiarize yourself with the concepts. reloadData is a method that eventually calls tableView:cellForRowAtIndexPath:.
You are generating infinite loop by calling reloadData inside
tableView:cellForRowAtIndexPath:
There is no need to clear data in a table view. You may want to ensure you properly implement -prepareForReuse in your UITableViewCell subclasses though, so that reused cells don't show old data.
It's BTW unclear why you want to remove the if (cell == nil) bit. If there is no cell to reuse (dequeue) then you need to create a new one. If you don't you'll return nil, which will cause an exception in UITableView.
You (99% of the time) can't call reloadData from tableView:cellForRowAtIndexPath:.
This is because reloadData eventually calls tableView:cellForRowAtIndexPath, so you get a never ending loop.
You need to explain more what you trying to do (and some extra code would be nice) in order to answer the question further.
reloadData calls all tableView data source , so you cann't call it in cellForRowAtIndexPath it will make an infinite loop.
I have a table which I am manipulating with a tableViewController (no nib, and the controller is creating the table behind the scenes)
I'm trying to delete a row from the table based on its row number; I can delete it from the array I use to create the cell in cellForRowAtIndexPath, but I get a strange error if I try to do the following, which is the same code as in tableView:commitEditingStyle:forRowAtIndexPath:
where it works fine
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i+1 inSection:1]
[self.tableView deleteRowsAtIndexPaths:[NSArray
arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
It gives an error
-[_WebSafeForwarder forwardInvocation:]
and then jumps out of the method but does not crash the app
Can anyone help?
You need to do that in this block.
[self.tableView beginUpdates];
///
[self.tableView endUpdates];
do notice one thing that when the the block reaches its end
- (void) numberOfRowsInSection:(NSInteger)section
will be called again. so u need to update the number of rows in table view also.
Hope this helps.
Thanks,
Madhup
Well you are missing a semi-colon on your first line.
When in doubt, clean up your syntax...
As a point of observation, most of the programmers I have worked with and talked to really hate UITableViewController. It really adds nothing to the functionality for the user and only obfuscates things that developers might really like to control... such as the position of the table via a XIB.
It's just a convenience class and in my experience, causes more issues than it prevents.
I have a UITableView with style "Grouped" which I use to set some options in my App. I'd like for one of the cells of this UITableView to only show up depending on whether another of this UITableView's cells is activated or not. If it's not, the first cell should show up (preferably with a smooth animation), if it is, the first cell should hide.
I tried returning nil in the appropriate -tableView:cellForRowAtIndexPath: to hide the cell, but that doesn't work and instead throws an exception.
I'm currently stuck and out of ideas how to solve this, so I hope some of you can point me in the right direction.
You should remove the data behind the hidden cells from the table view's data source.
For example, if you are using an array, when an action occurs that causes a cell to be hidden, you would remove the object for that row from the array. Then, as the table view's data source, the array will return one less total count and only return valid cells for every row in that count (no nil).
This approach may require maintaining a second array with all of the objects (including hidden).
To update the view, check out reloadRowsAtIndexPaths:withRowAnimation:.
Here's a handy post in which the author provides some source code for performing animations on the currently selected cell:
http://iphonedevelopment.blogspot.com/2010/01/navigation-based-core-data-application.html
He's using this in a NSFetchedResultsController context, but you can see how he's using various calls to add/remove cells & sections.
Now, in your case, you'll need to modify whatever array you're using to host the data used to generate the rows in your tableView when you "activate" your cell, then selectively use:
tableView:insertRowsAtIndexPaths:withRowAnimation:
tableView:deleteRowsAtIndexPaths:withRowAnimation:
tableView:insertSections:withRowAnimation:
tableView:deleteSections:withRowAnimation:
to adjust things accordingly (you can start with tableView:reloadData:, but it's inefficient).
I realize that the API can be a bit daunting, but take the time to read through it and understand what the various calls do. Understanding how the UITableView uses its datasource and delegate, as well as the chain of events that occur when cells are selected/deleted/etc., is important if you want to get things just right (and crash-free).
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:withRowAnimation:]; // or insertRowsAtIndexPaths:withAnimation:
[tableView endUpdates];
Before cellForRowAtIndexPath is called, numberOfRowsInSection is called. You should return the appropriate value of cells in the section there, so if you only want to show 1 cell, return one. The logic what cells are shown has to be implemented partially in both methods
I've got an NSFetchedResultsController as my data source and and I implement NSFetchedResultsControllerDelegate in my custom UITableViewController. I'm using sectionNameKeyPath to break my result set into multiple sections.
In one of my methods, I'm adding a couple of objects to the context, all of which are in a new section. At the moment where I save the the objects, the delegate methods are called properly. The order of events:
// -controllerWillChangeContent: fires
[self.tableView beginUpdates]; // I do this
// -controller:didChangeSection:atIndex:forChangeType: fires for section insert
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
// -controller:didChangeObject:atIndexPath:forChangeType:newIndexPath fires many times
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITavleViewRowAnimationFade]; // for each cell
// -controllerDidChangeContent: fires after all of the inserts
[self.tableView endUpdates]; // <--- Where things go terribly wrong!!!
On the last call, "endUpdates", the application always crashes with:
Serious application error. Exception was caught during Core Data change processing:
[NSCFArray objectAtIndex:]: index (5) beyond bounds (1) with userInfo (null)
It seems that the table updates are not in sync with the NSFetchedResultsController data in some way, and things blow up. I'm following the docs on NSFetchedResultsControllerDelegate, but it's not working. What's the right way to do it?
UPDATE: I've created a test project that exhibits this bug. You can download it at: NSBoom.zip
Tracing through the app, I note didChangeSection is called first, which inserts a whole section - and then didChangeObject is called repeatedly.
The problem is that in didChangeSection you insert a whole section, then right after before the table view is updated you are adding objects to that same section. This is basically a case of overlapping updates... (not allowed even in a begin/end updates block).
If you comment out the individual object insert, it all works:
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
If you comment out the section insert:, it doesn't work - but I have had less luck with insertRowsInSections working all the time, and it may well be because there is no section yet (which I'm sure is why you were inserting the section to begin with). You may have to detect either case to do inserts with the right granularity.
In general I've had a lot more luck reloading and inserting whole sections than rows, the table view seems very fiddly to me around those working. You can also try UITableViewRowAnimationNone which seems to operate successfully more often.
I'm having the same problem. I find that didChangeSection fires off twice. Once when you create the object for insert, and once when you actually save it. To me, it shouldn't call didChangeSection until save is called. Or at the very least, willChangeSection would be called on creation of the object, and didChangeSection called when it is saved.
Now I'm looking into the NSManagedObjectContextDidSaveNotification observer method. This isn't part of the NSFetchedResultsControllerDelegate protocol, but you can register to receive it. Perhaps that will only be called when I actually call save and not before.