I keep running into an assertion failure but I am not sure how to resolve.
Initially I did a proof of concept app to understand how this functions and had this code in it:
if (self.arrNonATIResults.count > 0) {
NSMutableArray* arrIndexPaths = [NSMutableArray array];
for(int i=0; i<[self.arrNonATIResults count]; i++) {
NSIndexPath *anIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
[arrIndexPaths addObject:anIndexPath];
}
[self.accountTable beginUpdates];
[self.accountTable deleteRowsAtIndexPaths:arrIndexPaths withRowAnimation:UITableViewRowAnimationTop];
self.arrNonATIResults = [NSMutableArray array];
[self.accountTable endUpdates];
}
It worked fine. Moving that code into my app I ran into this assertion failure:
Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (0) must be equal to the number of rows contained in that section before the update (3), plus or minus the number of rows inserted or deleted from that section (0 inserted, 2 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).
Here's where I am getting confused, self.arrNonATIResults (the table datasource) contains two objects (it's hardcoded) - so the table has only two rows. Where is the 3rd row in the error message coming from? I did read that if you remove all rows then you also have to remove the section as well. Is that the third item in the error message?
So I rewrote my initial code but this still shouldn't work because the arrIndexPath array is only going to include 2 NSIndexPaths. Can anyone point me in the right direction as to what I am doing wrong?
- (void) clearNonATITable {
if (self.arrNonATIResults.count > 0) {
NSMutableArray* arrIndexPaths = [NSMutableArray array];
for(int i=0; i<[self.arrNonATIResults count]; i++) {
NSIndexPath *anIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
[arrIndexPaths addObject:anIndexPath];
}
[self.accountTable beginUpdates];
for (int i=0; i<arrIndexPaths.count; i++) {
NSIndexPath* thisIndexPath = [arrIndexPaths objectAtIndex:i];
if (self.arrNonATIResults.count > 0) {
[self.accountTable deleteRowsAtIndexPaths:[NSArray arrayWithObjects:thisIndexPath, nil] withRowAnimation:UITableViewRowAnimationFade];
[self.arrNonATIResults removeObjectAtIndex:0];
} else {
NSIndexSet* setIndexSections = [[NSIndexSet alloc] initWithIndex:thisIndexPath];
[self.accountTable deleteSections:setIndexSections withRowAnimation:UITableViewRowAnimationFade];
}
}
[self.accountTable endUpdates];
}
}
[myArray removeAllObjects];
[tableView reloadData];
The issue was the app was using 3 datasources for this one tableview - the datasource would change based on a segment control selection. I just needed to determine which source it was and use it's count for the number of NSIndexPaths to remove.
Related
- (void)registerCellCallBack:(id<CellCallBack>)callBack {
int i = 0;
for(; i < _dataSource.count; ++i) {
id<CellCallBack> oldCallBack = _dataSource[i];
if([callBack key] <= [oldCallBack key]) {
break;
}
}
if(i < _dataSource.count) {
[_dataSource insertObject:callBack atIndex:i];
} else {
[_dataSource addObject:callBack];
}
[self.table beginUpdates];
[self.table insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
[self.table endUpdates]; //--crashed here called "NSInternalInconsistencyException(SIGABRT)"
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _dataSource.count;
}
The code which even was well in iOS9, crashed in iOS10, but not appear when I debug my code.
Crash info:
Invalid update: invalid number of rows in section 0. The
number of rows contained in an existing section after the update (7)
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 (1 inserted, 0 deleted) and plus or minus the number
of rows moved into or out of that section (0 moved in, 0 moved out).
You need to change as per below:
To
[self.table reloadData];
From
[self.table beginUpdates];
[self.table insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
[self.table endUpdates]; //--crashed here called "NSInternalInconsistencyException(SIGABRT)"
I have a UITableView with expandable sections. When a user goes to another view, I need all the expanded sections to collapse, which I'll need to put in the viewWillDisappear method.
I've found solutions only on how to delete all rows from a table view at once, but is there a way to delete all the rows from a specific section?
EDIT:
I have figured out a solution, but I'm not sure if it's optimal or can lead to inefficiencies in the future. Whenever a cell is expanded, it gets added to an NSMutableIndexSet. So in my viewWillDisappear method, I iterate over the expanded sections like so:
-(void)viewWillDisappear:(BOOL)animated
{
if (expandedSections.count != 0) {
NSLog(#"COLLAPSING CALLED");
[self.tableView beginUpdates];
NSUInteger section = [expandedSections firstIndex];
do
{
NSInteger rows;
NSMutableArray *tmpArray = [NSMutableArray array];
rows = [self tableView:self.tableView numberOfRowsInSection:section];
[expandedSections removeIndex:section];
for (int i=1; i<rows; i++) {
NSIndexPath *tmpIndexPath = [NSIndexPath indexPathForRow:i inSection:section];
[tmpArray addObject:tmpIndexPath];
}
[self.tableView deleteRowsAtIndexPaths:tmpArray withRowAnimation:UITableViewRowAnimationTop];
NSIndexPath *expandableCellIndexPath = [NSIndexPath indexPathForRow:0 inSection:section];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:expandableCellIndexPath];
cell.accessoryView = [DTCustomColoredAccessory accessoryWithColor:[self.colorHolder objectAtIndex:section] type:DTCustomColoredAccessoryTypeRight];
section = [expandedSections indexGreaterThanIndex:section];
} while (section != NSNotFound);
[self.tableView endUpdates];
}
}
Please let me know if this is a good solution or, if I'm suspecting correctly, if this will lead to slower transitions between views in the future when there are more rows in each expanded section. Any help or advice would be appreciated. Thanks.
If you want to animate changes, you will need to first update your data source (to return 0 for number of rows in the section) then remove section and add section at the same index path in one transaction between [tv beginUpdates] [tv endUpdates]
Otherwise just update the data source and reload the table on your way back to the VC (if you don't want any animations)
I did not read this in detail but surely the for loop should start at zero.
for (int i=0; i<rows; i++) {
NSIndexPath *tmpIndexPath = [NSIndexPath indexPathForRow:i inSection:section];
[tmpArray addObject:tmpIndexPath];
}
otherwise you will only delete all but the first cells in the section.
I have a custom UITableView I'm trying to add a row to the tableView. Here is my code:
- (IBAction)addRow:(id)sender
{
NSInteger indexPath = [self.rowArray count] - 1;
// I'm adding an object to an array of all the cell ids I have.
[self.rowArray insertObject:#"rowSix" atIndex:indexPath]; // rowSix is a cell ID
[self.myTableView beginUpdates];
[self.myTableView endUpdates];
}
This is the error that it outputs:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (8) must be equal to the number of rows contained in that section before the update (7), 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).'
Update
In cellForRowAtIndexPath:
NSString *cellID = [self.rowArray objectAtIndex:[indexPath row]];
customCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID forIndexPath:indexPath];
Try this one-
//Update data source with the object that you need to add
[tableDataSource addObject:newObject];
NSInteger row = //specify a row where you need to add new row
NSInteger section = //specify the section where the new row to be added,
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationRight];
[self.tableView endUpdates];
try this -
- (IBAction)addRow:(id)sender
{
[self.rowArray addObject:#"New Item"];
NSIndexPath *newIndexPath = [NSIndexPath indexPathForRow:[self.rowArray count] inSection:0];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
I can's seem to be able to remove all table rows. What seems to happen is that only the cells that are in view are removed and then the ones that were out of view move up. Also checked and my data source contains only the cels that are in view at the time of deletion. Heres the code
NSMutableArray *left = [[NSMutableArray alloc]init];
NSMutableArray *right = [[NSMutableArray alloc]init];
for(int i=0;i<dataSource.count;i++){
[dataSource removeObjectAtIndex:i];
NSIndexPath *ip = [NSIndexPath indexPathForRow:i inSection:0];
if(i%2==0)
[left addObject:ip];
else
[right addObject:ip];
}
[_view.tableView beginUpdates];
[_view.tableView deleteRowsAtIndexPaths:[NSArray arrayWithArray:left]
withRowAnimation:UITableViewRowAnimationLeft];
[_view.tableView deleteRowsAtIndexPaths:[NSArray arrayWithArray:right]
withRowAnimation:UITableViewRowAnimationRight];
[_view.tableView endUpdates];
What happens now is that only the cells in view are removed and the rest pop in after the removal (also the issue seems to pop up since the dataSource only contains the rows that are in view for some reason). What i want is to remove all the rows. Any ideas?
The problem is in the for loop condition. The dataSource.count gets smaller each iteration. Try this:
for(int i=0;i<dataSource.count;i++){
NSIndexPath *ip = [NSIndexPath indexPathForRow:i inSection:0];
if(i%2==0)
[left addObject:ip];
else
[right addObject:ip];
}
[dataSource removeAllObjects];
try something like this:
[_view.tableView beginUpdates];
UITableViewRowAnimation animation;
for(int i = dataSource.count - 1; i>= 0; i--)
{
[dataSource removeObjectAtIndex: i];
NSIndexPath* ip = [NSIndexPath indexPathForRow: i
inSection: 0];
if(i % 2 == 0)
{
animation = UITableViewRowAnimationLeft;
}
else
{
animation = UITableViewRowAnimationRight;
}
[_view.tableView deleteRowsAtIndexPaths: #[ip]]
withRowAnimation: animation];
}
[_view.tableView endUpdates];
You can do this in easy way...
[dataSource removeAllObjects];
[_view.tableView reloadData];
i think that you can put your dataSource = #[]; and [self.tableview reloadData]; and remove all cell in your tableview
I have a UITableView, where there is a UISegmentedControl in the header view. It should work exactly like in the App Store app: As the user scrolls, the title in the header scrolls off the screen but the segmentedControl sticks under the navigationBar.
When the user selects a segment, the section below the header should be reloaded with a nice UITableViewRowAnimation. However, as I call tableView:reloadSections:withRowAnimation:, the header view is animated as well, which I want to prevent, because it looks terrible.
Here's my code for this:
- (void)selectedSegmentIndexChanged:(UISegmentedControl *)sender
{
int index = sender.selectedSegmentIndex;
if (index < self.oldIndex) {
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationLeft];
} else if (index > self.oldIndex) {
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationRight];
}
self.oldIndex = index;
}
Anyone has an idea how to reload the section below the header without reloading the header itself?
Maybe you should try with
[self.tableView reloadRowsAtIndexPaths:[self.tableView indexPathsForVisibleRows] withRowAnimation:UITableViewRowAnimationLeft] //or UITableViewRowAnimationRight
However, I'm not sure but I think it can rise some error in the case where you have less rows to reload than previously.
Edit
I think you could deal with [tableView beginUpdates] and [tableView endUpdates] to solve your problem.
For example, you have 2 arrays of data to display. Let name them oldArray and newArray.
A sample of how what you could do :
- (void)selectedSegmentIndexChanged:(UISegmentedControl *)sender
{
[self.tableView setDataSource: newArray];
int nbRowToDelete = [oldArray count];
int nbRowToInsert = [newArray count];
NSMutableArray *indexPathsToInsert = [[NSMutableArray alloc] init];
for (NSInteger i = 0; i < nbRowToInsert; i++) {
[indexPathsToInsert addObject:[NSIndexPath indexPathForRow:i inSection:section]];
}
NSMutableArray *indexPathsToDelete = [[NSMutableArray alloc] init];
for (NSInteger i = 0; i < nbRowToDelete; i++) {
[indexPathsToDelete addObject:[NSIndexPath indexPathForRow:i inSection:section]];
}
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:indexPathsToDelete withRowAnimation:UITableViewRowAnimationLeft];
[self.tableView insertRowsAtIndexPaths:indexPathsToInsert withRowAnimation:UITableViewRowAnimationRight];
[self.tableView endUpdates];
}
If you are using Swift 2.0, feel free to use this extension.
Be warned: passing in the wrong oldCount or newCount will crash you program.
extension UITableView{
func reloadRowsInSection(section: Int, oldCount:Int, newCount: Int){
let maxCount = max(oldCount, newCount)
let minCount = min(oldCount, newCount)
var changed = [NSIndexPath]()
for i in minCount..<maxCount {
let indexPath = NSIndexPath(forRow: i, inSection: section)
changed.append(indexPath)
}
var reload = [NSIndexPath]()
for i in 0..<minCount{
let indexPath = NSIndexPath(forRow: i, inSection: section)
reload.append(indexPath)
}
beginUpdates()
if(newCount > oldCount){
insertRowsAtIndexPaths(changed, withRowAnimation: .Fade)
}else if(oldCount > newCount){
deleteRowsAtIndexPaths(changed, withRowAnimation: .Fade)
}
if(newCount > oldCount || newCount == oldCount){
reloadRowsAtIndexPaths(reload, withRowAnimation: .None)
}
endUpdates()
}
Try this:
BOOL needsReloadHeader = YES;
UIView *oldHeaderView = nil;
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
UIView *headerToReturn = nil;
if(needsReloadHeader == YES) {
headerToReturn = [[UIView alloc] init];
// ...
// custom your header view in this block
// and save
// ...
oldHeaderView = headerToReturn;
} else {
headerToReturn = oldHeaderView;
}
return headerToReturn;
}
Your just need to change 'needsReloadHeader' to 'NO' in other places.
An objective-c version of Intentss extension
#interface UITableView (Extensions)
- (void)reloadRowsInSection:(NSUInteger)sectionIndex withRowAnimation:(UITableViewRowAnimation)rowAnimation oldCount:(NSUInteger)oldCount newCount:(NSUInteger)newCount;
#end
#implementation UITableView (Extensions)
- (void)reloadRowsInSection:(NSUInteger)sectionIndex withRowAnimation:(UITableViewRowAnimation)rowAnimation oldCount:(NSUInteger)oldCount newCount:(NSUInteger)newCount {
NSUInteger minCount = MIN(oldCount, newCount);
NSMutableArray *insert = [NSMutableArray array];
NSMutableArray *delete = [NSMutableArray array];
NSMutableArray *reload = [NSMutableArray array];
for (NSUInteger row = oldCount; row < newCount; row++) {
[insert addObject:[NSIndexPath indexPathForRow:row inSection:sectionIndex]];
}
for (NSUInteger row = newCount; row < oldCount; row++) {
[delete addObject:[NSIndexPath indexPathForRow:row inSection:sectionIndex]];
}
for (NSUInteger row = 0; row < minCount; row++) {
[reload addObject:[NSIndexPath indexPathForRow:row inSection:sectionIndex]];
}
[self beginUpdates];
[self insertRowsAtIndexPaths:insert withRowAnimation:rowAnimation];
[self deleteRowsAtIndexPaths:delete withRowAnimation:rowAnimation];
[self reloadRowsAtIndexPaths:reload withRowAnimation:rowAnimation];
[self endUpdates];
}
#end
You're reloading the section, so clearly everything in the section will be reloaded (including the header).
Why not instead place the UISegmentedControl inside UITableView's tableHeaderView? This would allow for exactly the behavior you're after.
The simple answer is just don't reload the sections animated, just use UITableViewRowAnimationNone.
Right now you're using UITableViewRowAnimationLeft and UITableViewRowAnimationRight, which slides your section in and out as well.
However, even with UITableViewRowAnimationNone, rows will still be animated if the number of cells before the update differ from the ones after the update.
Also, a nice read on this topic, here.
Cheers.
Here's another way which you could use and still use animations.
Let's say you have a dynamic DataSource, which changes when you select something, and you want to update just the rows of that section, while leaving the section header on top, untouched.
/** I get the desired handler from the handler collection. This handler is just a
simple NSObject subclass subscribed to UITableViewDelegate and UITableViewDataSource
protocols. **/
id handler = [self.tableViewHandlers objectForKey:[NSNumber numberWithInteger:index]];
/** Get the rows which will be deleted */
NSInteger numberOfRows = [self.tableView numberOfRowsInSection:sectionIndex];
NSMutableArray* indexPathArray = [NSMutableArray array];
for (int rowIndex = 0; rowIndex < numberOfRows; rowIndex++){
[indexPathArray addObject:[NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex]];
}
/** Update the handler */
[self.tableView setDataSource:handler];
[self.tableView setDelegate:handler];
/** Get the rows which will be added */
NSInteger newNumberOfRows = [handler tableView:self.tableView numberOfRowsInSection:sectionIndex];
NSMutableArray* newIndexPathArray = [NSMutableArray array];
for (int rowIndex = 0; rowIndex < newNumberOfRows; rowIndex++){
[newIndexPathArray addObject:[NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex]];
}
/** Perform updates */
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:indexPathArray withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:newIndexPathArray withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
As a note, please stick to the specified order of operations, UITableView demands it.
If you have only one handler (datasource and delegate), it's easy to modify the above code to achieve the same results.