iOS UICollectionView, how to animate datasource change? - ios

I'm working with a UICollection view and would like to animate changes to it's datasource. New datasource shares a lot of items with the old one, and I would like to animate insertion of new items and deletion of items that are no longer in the datasource, while keeping all other cells intact:
Current: [1,2,8,9]
Updated: [1,2,3,9]
Changes: insert 3, delete 8
Are there examples of swapping datasources for UICollectionView with animation?
I tried this, which causes the collection view to flash, but no animations are being played:
[self.collectionView performBatchUpdates:^{
[self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
} completion:^(BOOL finished) {
}];
This method complains about numbers of items not matching:
-(void)setDatasource:(NSMutableArray *)datasource
{
NSMutableArray* _datasourceBackup = _datasource;
//find out what is being added and deleted
NSMutableArray* itemsToDelete = [NSMutableArray arrayWithArray:_datasource];
DLog(#"delete raw: %lu", (unsigned long)itemsToDelete.count);
[itemsToDelete removeObjectsInArray:datasource];
DLog(#"delete filtered: %lu", (unsigned long)itemsToDelete.count);
NSMutableArray* itemsToAdd = [NSMutableArray arrayWithArray:datasource];
DLog(#"add raw: %lu", (unsigned long)itemsToAdd.count);
[itemsToAdd removeObjectsInArray:_datasource];
DLog(#"add filtered: %lu", (unsigned long)itemsToAdd.count);
//1 update datasource
_datasource = datasource;
if(_datasourceBackup.count == 0)
{
//original load
[self.collectionView reloadData];
}else
{
[self.collectionView performBatchUpdates:^{
//convert items to add/delete into index paths and give them to collection view to animate
NSMutableArray* indexPathsToDelete = [NSMutableArray array];
for(NSString* identifier in itemsToDelete)
{
NSInteger index = [_datasourceBackup indexOfObject:identifier];
[indexPathsToDelete addObject: [NSIndexPath indexPathForRow:index inSection:0]];
}
if(indexPathsToDelete.count > 0 )
{
[self.collectionView deleteItemsAtIndexPaths:indexPathsToDelete];
}
NSMutableArray* indexPathsToInsert = [NSMutableArray arrayWithCapacity:itemsToAdd.count];
for(NSString* identifier in itemsToAdd)
{
NSInteger index = [_datasource indexOfObject:identifier];
[indexPathsToInsert addObject: [NSIndexPath indexPathForRow:index inSection:0]];
}
if(indexPathsToInsert.count > 0 )
{
[self.collectionView insertItemsAtIndexPaths:indexPathsToInsert];
}
} completion:nil];
}
}

Related

Collectionview performBatchUpdates crash

I am trying to add new items to my collection view using insertItemsAtIndexPaths. My app crashes at performBatchupdate
- (void) addItems {
NSArray *newProducts = #[#"1",#"2",#"3",#"4"];
[self.collectionView performBatchUpdates:^{
NSMutableArray *arrayWithIndexPaths = [NSMutableArray array];
for (NSInteger index = self.array.count; index < (self.array.count + newProducts.count); index++) {
[arrayWithIndexPaths addObject:[NSIndexPath indexPathForRow:index inSection:0]];
}
[self.array addObjectsFromArray:newProducts];
[self.collectionView insertItemsAtIndexPaths:arrayWithIndexPaths];
}
completion:nil];
}
Following is the crash log:
* Assertion failure in -[UICollectionView _createPreparedCellForItemAtIndexPath:withLayoutAttributes:applyAttributes:]
This Assertion happens when cell is not registered with the collectionview. I am registering my cell.
This worked for me:
If Collection view is empty reload else insertItems.
- (void)addItems {
NSArray *newProducts = #[#"1",#"2",#"3",#"4"];
NSMutableArray *arrayWithIndexPaths = [NSMutableArray array];
for (NSInteger index = self.array.count; index < (self.array.count + newProducts.count); index++) {
[arrayWithIndexPaths addObject:[NSIndexPath indexPathForRow:index inSection:0]];
}
if (self.array) {
[self.array addObjectsFromArray:newProducts];
[self.collectionView performBatchUpdates:^{
[self.collectionView insertItemsAtIndexPaths:arrayWithIndexPaths];
}
completion:nil];
}
else {
self.array = [[NSMutableArray alloc] initWithArray:newProducts];
[self.collectionView reloadData];
}
}

ios tableView reloadRowsAtIndexPaths not working

I have an issue where I have a UITableViewController and when the view appears I do some calculations asynchronously which should result in the updating of specific rows in the table.
Inside the viewWillAppear function I calculate the necessary rows that need to be updated as follows:
- (void)reloadPaths:(NSMutableDictionary *)bookingsDict
{
NSMutableArray* indexArray = [NSMutableArray array];
int i = 0;
for (NSString *dateKey in self.eventsToShow) {
NSMutableDictionary *venues = [bookingsDict objectForKey:dateKey];
if (venues) {
int j = 0;
NSArray *events = [self.eventsToShow objectForKey:dateKey];
for (DCTEvent *event in events) {
NSString *venueIdString = [NSString stringWithFormat:#"%#", event.eventVenueId];
if ([venues objectForKey:venueIdString]) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:j inSection:i];
[indexArray addObject:indexPath];
}
j++;
}
}
i++;
}
[self.tableView reloadRowsAtIndexPaths:indexArray withRowAnimation:UITableViewRowAnimationFade];
}
However I notice that the cellForRowAtIndexPath function is never called after and the cells do not get updated correctly. Any idea what the issue might be?
EDIT: I just noticed that if I scroll out of the view of the cell that is supposed to get updated then it gets updated when I scroll back into view. Is there no way to have it update while in view?
How about wrap it?
[tableView beginUpdates];
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
[tableView endUpdates];
Here are some similar problems I have found.
cellForRowAtIndexPath isn't called after reloadRowsAtIndexPaths
UITableView's reloadRowsAtIndexPaths: (NSArray *) indexPaths failing to cause a reload unless you call it twice?
Hope this helps.
EDIT : I think you are not getting indexPath check section might be constant as you are increasing while each object gets traversed:
- (void)reloadPaths:(NSMutableDictionary *)bookingsDict
{
NSMutableArray* indexArray = [NSMutableArray array];
int i = 0;
for (NSString *dateKey in self.eventsToShow) {
NSMutableDictionary *venues = [bookingsDict objectForKey:dateKey];
if (venues) {
int j = 0;
NSArray *events = [self.eventsToShow objectForKey:dateKey];
for (DCTEvent *event in events) {
NSString *venueIdString = [NSString stringWithFormat:#"%#", event.eventVenueId];
if ([venues objectForKey:venueIdString]) {
//changed here as section might be constant as i think it might be
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:j inSection:0];
[indexArray addObject:indexPath];
}
j++;
}
}
i++;
}
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:indexArray withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
}
Try this :
//your code here
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:indexArray withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
For me pushing my reload code to the main queue solved the problem
DispatchQueue.main.async {
self.tableView.reloadRows(at: [indexPath], with: .automatic)
}

TableView updates not happening

I am inserting and removing rows at random from a tableview. However, if the rows I want to remove are not in view, they are not actually removed in the UI when the given row does come back to view.
If I add and remove the rows only on visible rows, it works just fine. What could be causing this problem?
- (void)sectionHeaderView:(BaseSectionHeaderView *)sectionHeaderView sectionOpened:(NSInteger)sectionOpened {
LogInfo(#"opening section:%ld",sectionOpened);
[_dataSource setSectionAtIndex:sectionOpened Open:YES];
/*
Create an array containing the index paths of the rows to insert: These correspond to the rows for each quotation in the current section.
*/
NSInteger countOfRowsToInsert = [_dataSource tableView:_tableView numberOfRowsInSection:sectionOpened];
NSMutableArray *indexPathsToInsert = [[NSMutableArray alloc] init];
for (NSInteger i = 0; i < countOfRowsToInsert; i++) {
[indexPathsToInsert addObject:[NSIndexPath indexPathForRow:i inSection:sectionOpened]];
}
/*
Create an array containing the index paths of the rows to delete: These correspond to the rows for each quotation in the previously-open section, if there was one.
*/
NSMutableArray *indexPathsToDelete = [[NSMutableArray alloc] init];
if (self.openSection.openSectionHeaderView != nil) {
[_openSection.openSectionHeaderView toggleOpenWithUserAction:NO];
// NSInteger countOfRowsToDelete = [_purchaseInvoiceListTable.tableView numberOfRowsInSection:self.openSection.openSectionIndex];
NSInteger countOfRowsToDelete = [_dataSource tableView:_tableView numberOfRowsInSection:self.openSection.openSectionIndex];
[_dataSource setSectionAtIndex:self.openSection.openSectionIndex Open:NO];
for (NSInteger i = 0; i < countOfRowsToDelete; i++) {
[indexPathsToDelete addObject:[NSIndexPath indexPathForRow:i inSection:self.openSection.openSectionIndex]];
}
}
// style the animation so that there's a smooth flow in either direction
UITableViewRowAnimation insertAnimation;
UITableViewRowAnimation deleteAnimation;
if (self.openSection.openSectionHeaderView == nil || sectionOpened < self.openSection.openSectionIndex) {
insertAnimation = UITableViewRowAnimationTop;
deleteAnimation = UITableViewRowAnimationBottom;
}
else {
insertAnimation = UITableViewRowAnimationBottom;
deleteAnimation = UITableViewRowAnimationTop;
}
// apply the updates
[_tableView beginUpdates];
[_tableView insertRowsAtIndexPaths:indexPathsToInsert withRowAnimation:insertAnimation];
[_tableView deleteRowsAtIndexPaths:indexPathsToDelete withRowAnimation:deleteAnimation];
[_tableView endUpdates];
self.openSection.openSectionIndex = sectionOpened;
self.openSection.openSectionHeaderView = sectionHeaderView;
self.openSection.sectionHeight = [NSNumber numberWithFloat:sectionHeaderView.frame.size.height];
LogInfo(#"sectionOpened:%ld",sectionOpened);
}
i think you should reload the table view by this...
[tableView reloadData];
and if from this it will not give the expected result then remove the object from array that fill the table view's row. i think this will definitely work...
tell me is it working or not...

Crashes with UITableView

I am displaying an array objects in a UITableView with inserts, updated and deletes and occasionally I get the exception below even though I am carefully handling the changes. I have the full code example on GitHub.
https://github.com/brennanMKE/TableMaddness
This sample project uses objects which implement NSCoding and NSCopying as well as isEqual so that updating the table with inserts, updates and deletes can simulate what I am doing in a real app which is having the same problem. I am avoiding using Core Data which would use NSFetchedResultsController so I'd like know if there is something I should use if I am not using Core Data.
Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-2903.23/UITableView.m:1330
2013-11-25 14:43:20.217 TableMaddness[13411:70b] 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 (4) must be equal to the number of rows contained in that section before the update (4), 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).'
Below is the related code.
// determine items which need to be inserted, updated or removed
NSMutableArray *inserts = [#[] mutableCopy];
NSMutableArray *deletes = [#[] mutableCopy];
NSMutableArray *reloads = [#[] mutableCopy];
// look for inserts
for (NSUInteger row=0; row<fetchedItems.count; row++) {
SSTItem *item = fetchedItems[row];
if (![self.currentItems containsObject:item]) {
// inserts are items which are not already in self.items
[inserts addObject:[NSIndexPath indexPathForRow:row inSection:0]];
}
else {
NSUInteger otherIndex = [self.currentItems indexOfObject:item];
SSTItem *otherItem = [self.currentItems objectAtIndex:otherIndex];
if (![item.modified isEqualToDate:otherItem.modified]) {
[reloads addObject:[NSIndexPath indexPathForRow:row inSection:0]];
}
}
}
// look for deletes
for (NSUInteger row=0; row<self.currentItems.count; row++) {
SSTItem *item = self.currentItems[row];
if (![fetchedItems containsObject:item]) {
[deletes addObject:[NSIndexPath indexPathForRow:row inSection:0]];
}
}
static NSString *lock = #"LOCK";
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (kDelay / 4) * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
// lock is required to prevent inconsistencies when changing view orientation during rotation
#synchronized(lock) {
self.currentItems = fetchedItems;
NSUInteger numberOfRowsInSection = [self tableView:self.tableView numberOfRowsInSection:0];
DebugLog(#"numberOfRowsInSection: %li", numberOfRowsInSection);
DebugLog(#"self.items: %li", self.currentItems.count);
MAAssert(self.currentItems.count == numberOfRowsInSection, #"Match is required");
if (inserts.count || deletes.count || reloads.count) {
[self.tableView beginUpdates];
#ifndef NDEBUG
for (NSIndexPath *indexPath in inserts) {
DebugLog(#"Inserting at %li", (long)indexPath.row);
}
for (NSIndexPath *indexPath in deletes) {
DebugLog(#"Deleting at %li", (long)indexPath.row);
}
for (NSIndexPath *indexPath in reloads) {
DebugLog(#"Reloading at %li", (long)indexPath.row);
}
#endif
[self.tableView insertRowsAtIndexPaths:inserts withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView deleteRowsAtIndexPaths:deletes withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView reloadRowsAtIndexPaths:reloads withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
}
}
index++;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kDelay * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self runNextUpdate];
});
});
- (void)runNextUpdate {
if (index >= self.dataSets.count) {
index = 0;
[self runNextUpdate]; // remove this line
return; // or add this line.
}
Because if this condition is true, your code will run twice almost at the same time, but the data source has not been updated.

Expanding and collapsing tableView Sections

I want to expand/collapse table view sections.I googled and found a code and its working fine.but the problem is the previously opened section is not closed when a new section is opened.Thanks
- (void)sectionHeaderTapped:(UITapGestureRecognizer *)gestureRecognizer{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:gestureRecognizer.view.tag];
if (indexPath.row == 0) {
BOOL collapsed = [[arrayForBool objectAtIndex:indexPath.section] boolValue];
collapsed = !collapsed;
[arrayForBool replaceObjectAtIndex:indexPath.section withObject:[NSNumber numberWithBool:collapsed]];
//reload specific section animated
NSRange range = NSMakeRange(indexPath.section, 1);
NSIndexSet *sectionToReload = [NSIndexSet indexSetWithIndexesInRange:range];
[self.aTableView reloadSections:sectionToReload withRowAnimation:UITableViewRowAnimationFade];
}
}
I tried the following code which is working but the animation is not cool as all the sections are reloading.
NSMutableArray *isSectionTouched =[[NSMutableArray alloc]initWithCapacity:arrayForBool.count];
isSectionTouched=[arrayForBool mutableCopy];
for(int i = 1; i <[arrayForBool count] ; i ++){
if(i != gestureRecognizer.view.tag){
[isSectionTouched replaceObjectAtIndex:i withObject:[NSNumber numberWithBool:NO]];
}else{
if ([[isSectionTouched objectAtIndex:gestureRecognizer.view.tag]boolValue]==YES) {
[isSectionTouched replaceObjectAtIndex:gestureRecognizer.view.tag withObject:[NSNumber numberWithBool:NO]];
}else if ([[isSectionTouched objectAtIndex:gestureRecognizer.view.tag]boolValue]==NO){
[isSectionTouched replaceObjectAtIndex:gestureRecognizer.view.tag withObject:[NSNumber numberWithBool:YES]];
}
}
}
arrayForBool=isSectionTouched;
NSRange range = NSMakeRange(1,arrayForBool.count - 1);
NSIndexSet *sectionToReload = [NSIndexSet indexSetWithIndexesInRange:range];
[self.tableView reloadSections:sectionToReload withRowAnimation:UITableViewRowAnimationFade];
Just before you replace the selected index path, iterate over arrayForBool and set each item to NO. You could hold a property to store the currently open section index but unless you have hundreds of sections it isn't worth it.

Resources