UICollectionView cellForItemAtIndexPath is called but data source is empty - ios

I have a custom tab on top of view which a user can switch two view back and forth. Both view contains UICollectionView dynamically filled with data from a server. In my understanding, if numberOfItemInSection return 0 when [collectionView reloadData] is called, cellForItemAtIndexPath won't be called. In other word, if cellForItemAtIndexPath is being called, it means there is some data in source. Below is some code I used when switch to one view to other view.
- (void)refresh
{
[self.collectionView setContentOffset:CGPointZero];
[self.data removeAllObjects];
[self.refreshControl beginRefreshing];
if (self.collectionView.contentOffset.y == 0) {
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^(void){
self.collectionView.contentOffset = CGPointMake(0, -MPViewHeight(self.refreshControl));
} completion:^(BOOL finished){
}];
}
[self.collectionView scrollRectToVisible:CGRectMake(0, 0, 1, 1) animated:YES];
[self requestToServerAtIndex:0];
}
- (void) requestToServerAtIndex:(NSInteger)index
{
#weakify(self)
[[self.serverHelper getData:index requestCount:20] subscribeNext:^(id JSON) {
#strongify(self)
NSArray *data = [ParserForJSON parse:JSON];
if(data.count != 20) {
self.moreData = NO;
}
[self.data addObjectsFromArray:data];
[self.collectionView reloadData];
[self.refreshControl endRefreshing];
} error:^(NSError *error) {
}];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return self.data.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CustomData *data = [self.data objectAtIndex:indexPath.row];
CustomCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
///.... some cell configuration
return cell;
}
However, I got random crash on cellForItemAtIndexPath when it tried to access data source which was happened to be empty. Surprisingly, the data source was not empty when I checked on console at that point. So my wild guess is there was some timing issue. I would like to get some in depth explanation on this ambiguous behavior.

When you call refresh method, you remove all data objects, so when your cellForItemAtIndexPath was called, there's no data in data source. That's why when call id object = [data objectAtIndex:indexPath.row] return nil. So make sure after requestToServerAtIndex make the new datas realy then remove the old objects and update the UI.
EDIT:
Try to remove [self.collectionView scrollRectToVisible:CGRectMake(0, 0, 1, 1) animated:YES]; to test, this method should call cellForItemAtIndexPath

Related

Collectionview Cell Delegate method not called on scrollToItemAtIndexPath?

I use scrollToItemAtIndexPath in the viewDidLayoutSubviews part of my controller to allow me to scroll a user to a specific cell.
My cells are supposed to make network calls for their params when they are loaded as part of the cellForItemAtIndexPath
The issue is that putting a print statement in cellForItemAtIndexPath it appears it's never called? What's the cause of the conflict here and the solution to make it work? Code as follows:
- (void)viewDidLayoutSubviews {
CDCChannelCollectionView *scrollView;
if (self.view == [self mixMonitorView]) {
scrollView = [[self mixMonitorView] scrollView];
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection: self.selectedChanIndex];
[scrollView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
}
}
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
CDCChannelStrip *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"cell" forIndexPath:indexPath];
if (self.view == self.mixMonitorView) {
NSInteger chanInt = indexPath.section;
NSNumber *chanNum = [NSNumber numberWithInteger:chanInt];
NSNumber *chanNameNumber = [NSNumber numberWithInteger:chanInt + 1];
NSString *chanName = [NSString stringWithFormat:#"CH %#", chanNameNumber];
cell.channelNumber = chanInt;
cell.channelName.text = chanName;
[self getParameters:(chanNum)];
[self.mixMonitorView setChannelsStripToType:(cell)];
cell.clipsToBounds = YES;
return cell;
}
}
scrollToItemAtIndexPath will not call datasource methods unless it is scrolling to a collectionViewCell which is not visible before scrolling. You need to call reloadData if it is scrolling to an already visible cell
Datasource method will call on reloading the collection view.
Try below
[yourCollectionViewName reloadData];

UIGestureRecognizer on custom UITableViewCell. Tapping fires on multiple cells

I've searched high and low to no avail on this odd issue. I have a custom subclassed UITableViewCell. Two views for a front and back, and when tapped the cell flips over. This is done like so:
- (IBAction)cellFrontTapped:(id)sender {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
if (_cellFlag == 0)
{
_cellFlag = 1;
[UIView transitionFromView:_cellFrontView toView:_cellBackView
duration:0.5
options:UIViewAnimationOptionTransitionFlipFromTop | UIViewAnimationOptionShowHideTransitionViews
completion:NULL];
[self performSelector:#selector(cellBackTapped:) withObject:self afterDelay:15.0];
}
}
- (IBAction)cellBackTapped:(id)sender {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
if (_cellFlag == 1)
{
_cellFlag = 0;
[UIView transitionFromView:_cellBackView toView:_cellFrontView
duration:0.5
options:UIViewAnimationOptionTransitionFlipFromBottom | UIViewAnimationOptionShowHideTransitionViews
completion:NULL];
}
}
This works fine with just a few cells. Up to a few pages worth. But when I load a larger data set, tapping on one cell flips it over like expected, but also flips over other cells.
I realize this is because the cells are being re-used by dequeueReusableCellWithIdentifier but I cannot decipher how to prevent it.
I've tried implementing it as one UIGestureRecognizer. I've tried utilizing didSelectRowAtIndexPath like this:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
MyTableViewCell *cell = (MyTableViewCell*)[tableView cellForRowAtIndexPath:indexPath];
if (cell.cellFlag == 0)
{
[cell cellFrontTapped];
}
else
{
[cell cellBackTapped];
}
}
These all work to flip the cell properly, but the tap event always flips other cells in the list. The only solution I've found is to not use dequeueReusableCellWithIdentifier like so:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"MyTableViewCell"];
if (cell == nil) {
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"MyTableViewCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
}
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
Here's my current subclass implementation:
#implementation MyTableViewCell
- (void)awakeFromNib {
// Adding the recognizer here produced the same result
//UIGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(cellTapped)];
//tap.delegate = self;
//[self addGestureRecognizer:tap];
_cellFlag = 0;
}
- (void)cellFrontTapped {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
_cellFlag = 1;
[UIView transitionFromView:_cellFrontView toView:_cellBackView
duration:0.5
options:UIViewAnimationOptionTransitionFlipFromTop | UIViewAnimationOptionShowHideTransitionViews
completion:NULL];
[self performSelector:#selector(cellBackTapped) withObject:self afterDelay:15.0];
}
- (void)cellBackTapped {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
_cellFlag = 0;
[UIView transitionFromView:_cellBackView toView:_cellFrontView
duration:0.5
options:UIViewAnimationOptionTransitionFlipFromBottom | UIViewAnimationOptionShowHideTransitionViews
completion:NULL];
}
-(void)cellTapped
{
if (_cellFlag == 0)
{
[self cellFrontTapped];
}
else
{
[self cellBackTapped];
}
}
#end
Here's the `configureCell:atIndexpath:
- (void)configureCell:(MyTableViewCell*)cell atIndexPath:(NSIndexPath*)indexPath
{
NSManagedObject *t = [[self fetchedResultsController] objectAtIndexPath:indexPath];
//here i set the labels and such
cell.cellBackAmountLabel.text = [t.amount formattedAmount];
//added
if ([self.flippedCellIndexes containsObject:indexPath])
{
[cell.contentView bringSubviewToFront:cell.cellBackView];
}
else
{
[cell.contentView bringSubviewToFront:cell.cellFrontView];
}
}
I'm at a loss. Not using dequeueReusableCellWithIdentifier makes a significant performance impact in my case. Help!
I would recommend separating your model from your view. What this means is that you should keep track of which cells should render as flipped, but not on the cell itself. Lets say you declare an NSArray (probably NSMutableArray, even) called flippedIndexes in your view controller.
If your data set is as big as you say, then probably you should use a sparse array instead of NSArray. Sparse arrays can easily be implemented with NSMutableDictionary. NSMutableSet could also work.
When dequeueing the cell, you would then check which cells should be flipped, probably at that configureCell:atIndexPath: method of yours. Basically, if the indexPath shows on your sparse array or set you render the cell as flipped. This would imply dropping the cellFlag property you have declared on your cell and toggling its state according to the model I've been mentioning. For flipping a cell, check the flippedIndexes property for the given indexPath and act accordingly. Something like this:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
MyTableViewCell *cell = (MyTableViewCell*)[tableView cellForRowAtIndexPath:indexPath];
if (![self.flippedIndexes containsObject:indexPath])
{
[cell cellFrontTapped];
[self.flippedIndexes addObject:indexPath];
}
else
{
[cell cellBackTapped];
[self.flippedIndexes removeObject:indexPath];
}
}
Here I'm assuming the use of NSMutableSet for the flippedIndexes property. Notice that this will only work properly if you also check the model in configureCell:atIndexPath:, as otherwise you'll have cells magically clearing when scrolling.
Moral of the story is: don't store state information in queued cells.

Upload in UiCollectionView Section

I have UICollectionView with 2 sections: first is the Upload Cells with its progress (and i need to remove it after uploading), second one has the all photos;
When im adding photo, i use KVO method
NSDictionary *uploadInfo = #{#"progress": progress, #"image": image, #"task": uploadTask, #"params": params};
[[NSNotificationCenter defaultCenter] postNotificationName:#"APIClientDidUploadingPhotoNotification" object:uploadInfo];
notification method get it
- (void)photoUploadAdded:(NSNotification*)notofication
{
NSDictionary *uploadInfo = [notofication object];
NSLog(#"Added photo upload: %#", uploadInfo[#"progress"]);
if (!self.photoUploadQueue) {
self.photoUploadQueue = [NSMutableArray array];
}
[self.photoUploadQueue addObject:uploadInfo];
dispatch_async(dispatch_get_main_queue(), ^
{
[self.collectionView reloadData];
});
}
in cellforItem i use
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell;
if (indexPath.section == 0) {
cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"UploadCell" forIndexPath:indexPath];
[((PhotoUploadCell*)cell) setDelegate:self];
if ([self.photoUploadQueue count]>0)
[((PhotoUploadCell*)cell) setUploadInfo:self.photoUploadQueue[indexPath.row]];
...
}
and when photo is loaded i remove it
- (void)uploadCompletedForCell:(id)cell
{
NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell];
[self.photoUploadQueue removeObjectAtIndex:indexPath.row];
//[self.collectionView reloadData];
}
it's not working as i expect,
This controller dealloceted sometime and notification don't work. i feel it is not the best solution. Maybe i should use ReactiveCocoa (but im not use it before) or some another solution

Reload UITableView Header View without blink

I have a problem with header view while reloading UITableView. Actually problem is with unexpected blinking the entire view just after reload.
I want dynamically change the height of the view and this animation is definitely unwanted.
Is it possible to rid of this.
To reload section I use:
[self.table reloadSections:indexSet withRowAnimation:UITableViewRowAnimationNone];
- (void)swipeAction:(UIPanGestureRecognizer *)gesture
{
CGFloat posY = [gesture translationInView:self.view].y;
self.dynamicHeight = posY;
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndex:0];
[self reloadSection:indexSet];
}
- (void)reloadSection:(NSIndexSet *)indexSet
{
[self.tblContainer beginUpdates];
[self.tblContainer reloadSections:indexSet withRowAnimation:UITableViewRowAnimationNone];
[self.tblContainer endUpdates];
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
UIViewController *vc = self.controllers[section];
if ([self.childViewControllers containsObject:vc]) {
} else {
[self addChildViewController:vc];
[vc didMoveToParentViewController:self];
}
return vc.view;
}
- (CGFloat)heightCell
{
return MIN_HEIGHT + dynamicHeight;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return [self.controllers[section] heightCell];
}
Hmm probably you should use this approach try to indicate when you start and end update of table view :
[self.tableView beginUpdates];
[self.table reloadSections:indexSet withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];

Removing the last Cell in UICollectionView makes a Crash

Hi I'm working with a Custom UICollectionView (https://github.com/SureCase/WaterfallCollectionView) where everything works fine. Now I'm setting up delete items from the UICollectionView, and I can delete them fine. The problem comes when I'm trying to delete the last item of the section.
It gives the following error.
*** Assertion failure in -[UICollectionViewData layoutAttributesForSupplementaryElementOfKind:atIndexPath:], /SourceCache/UIKit/UIKit-2935.137/UICollectionViewData.m:787
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'no UICollectionViewLayoutAttributes instance for -layoutAttributesForSupplementaryElementOfKind: UICollectionElementKindSectionHeader at path <NSIndexPath: 0xc000000000000016> {length = 2, path = 0 - 0}'
The Code I'm using to delete items is the following:
- (void) alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 0){
NSLog(#"Erasing Objects!");
//First I remove Items from DataSource, and after from Collection View
[self deleteItemsFromDataSourceAtIndexPaths:selectedIndexes];
[self.cv deleteItemsAtIndexPaths:selectedIndexes];
[selectedIObjectsIndexes removeAllObjects];
[selectedIndexes removeAllObjects];
EditUICollection=NO;
EasyMediaGreenView.hidden = YES;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.7 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self.cv reloadData];
});
}else{
NSLog(#"Not delete");
}
}
So first I'm removing items from DataSource and then from de Collection View. Here the Code for removing from Data Source.
-(void)deleteItemsFromDataSourceAtIndexPaths:(NSArray *)itemPaths
{
NSMutableIndexSet *indexSet = [NSMutableIndexSet indexSet];
for (NSIndexPath *itemPath in itemPaths) {
[indexSet addIndex:itemPath.row];
}
[idObject removeObjectsAtIndexes:indexSet];
[typeObject removeObjectsAtIndexes:indexSet];
[urlObject removeObjectsAtIndexes:indexSet];
[textObject removeObjectsAtIndexes:indexSet];
}
Why is this happening when I try to remove last object, when all other objects are removed correctly? I would like to understand this part, & any idea how to fix this? Thanks to all.
I Found a solution!
I was always returning 1 for section, so even with no items the CollectionView was being constructed, and asking to build a Header, like this:
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView
viewForSupplementaryElementOfKind:(NSString *)kind
atIndexPath:(NSIndexPath *)indexPath; {
So I counted the items in my data array, and if the array is empty, there shouldn't be a section.
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
if(idObject.count>0){ return 1; }
else{ return 0; }
}
So where I'm deleting the items from my data array, I can check if the array is empty, if is not empty I'll delete items, if it is, I'll delete the entire section.
NSLog(#"Erasing Objects!");
[self deleteItemsFromDataSourceAtIndexPaths:selectedIndexes];
if(idShadeInside.count > 0){
[self.cv deleteItemsAtIndexPaths:selectedIndexes];
}
else{
//Creating an IndexSet with the Section to Erase
NSMutableIndexSet *indexSet = [NSMutableIndexSet indexSet];[indexSet addIndex:0];
[self.cv deleteSections:indexSet];
}
So problem fixed!
i have done this features, here i provides you a simple code to delete cell from Collection View.
Just pass you index in integer here.
-(void)remove:(int)i {
#try {
[self.collection performBatchUpdates:^{
[self.arrayThumbImg removeObjectAtIndex:i];
NSIndexPath *indexPath =[NSIndexPath indexPathForRow:i inSection:0];
[self.collection deleteItemsAtIndexPaths:[NSArray arrayWithObject:indexPath]];
} completion:^(BOOL finished) {
}];
}
#catch (NSException *exception) {
NSLog(#"Error: %#",exception);
}
#finally {
}
}

Resources