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
Related
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
I've been battling with this issue for several days. I'm not an Obj-C expert but so far I was able to find my way until now :)
Basically I update a property of my tableViewController but it only updates the cell several seconds after the property was updated. I suspect an issue with cells being recycled but I thought I prevented that in my code.
As a note: [event updateThumbs] is a method that create a subview after downloading many thumbnails and combining them to create multiple buttons.
here is my code. Thanks for any help:
- (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"EventCell"];
//self.feedEvents is an NSMutableArray containing list of events
Event *event = [self.feedEvents objectAtIndex:indexPath.section];
event.delegate = self;
if (event.thumbsView == nil) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// The following method builds a view by combining subviews of buttons with thumbnails
// downloaded from back end.
[event updateThumbs];
NSLog(#"loaded %# for cell %ld",event.description,(long)indexPath.section);
[self.feedEvents replaceObjectAtIndex:indexPath.section withObject:event];
});
}
if (cell.contentView.subviews.count > 0) {
[cell.contentView.subviews[0] removeFromSuperview];
}
if (event) {
[cell.contentView addSubview:event.thumbsView];
}
return cell;}
******* UPDATE ****
I'm moving a bit forward I think. I changed the structure of the code by doing the a-sync work outside of the cellForRowAtIndex. It now happens in the method that calls the backend to populate the underlying property. But still, it doesn't work. The cells is updated only 4 to 6 seconds after the data is loaded. Here is what I do now:
- (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"EventCell"];
//self.feedEvents is an NSMutableArray containing list of events
Event *event = [self.feedEvents objectAtIndex:indexPath.section];
event.delegate = self;
if (cell.contentView.subviews.count > 0) {
[cell.contentView.subviews[0] removeFromSuperview];
}
if (event) {
[cell.contentView addSubview:event.thumbsView];
}
return cell;
}
and in the call to the back-end in the same Class
[self.feedEvents enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
Event *event = (Event *) obj;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
//a-sync work
[event updateThumbs];
dispatch_async(dispatch_get_main_queue(), ^{
[self.feedEvents replaceObjectAtIndex:idx withObject:event];
[self.tableView reloadData];
});
});
}];
I am trying to make some photoPicker with CollectionView.
Have
allowsMultipleSelection = YES
Using following method
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
selectedPictures = [NSMutableArray array];
[selectedPictures addObject:[imagesArray objectAtIndex:indexPath.item]];
NSLog(#"Selected list:\n %#", selectedPictures);
NSLog(#"Objects in Array %i", selectedPictures.count);
}
While I am selecting cells, it's always adding to MutableArray only one object according it's indexPath. What could be an issue?
Why don't u keep the selectedPictures as a member variable
in your code
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
selectedPictures = [NSMutableArray array]; //keep on creation the new array on each selection
[selectedPictures addObject:[imagesArray objectAtIndex:indexPath.item]]; //adding the selected images means single image
NSLog(#"Selected list:\n %#", selectedPictures);
NSLog(#"Objects in Array %i", selectedPictures.count);
}
try this
put his in viewDidLoad
- (void)viewDidLoad
{
selectedPictures = [[NSMutableArray alloc]init]; //initilise hear
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
// selectedPictures = [NSMutableArray array]; //keep on creation the new array on each selection
[selectedPictures addObject:[imagesArray objectAtIndex:indexPath.item]]; //adding the selected images means single image to already initialised array
NSLog(#"Selected list:\n %#", selectedPictures);
NSLog(#"Objects in Array %i", selectedPictures.count);
}
Hope this helps u .. :)
it may be caused by not calling super. While the documentation for UICollectionReusableView fails to mention this, the documentation for UITableViewCell, which has the same method, does.
- (void)prepareForReuse
{
[super prepareForReuse]
// Your code here.
}
Old Answer:
This may be a bug with the UICollectionView.
What's happening is cells that were previously selected are being reused and maintain the selected state. The collection view isn't setting selected to "NO".
The solution is to reset the the selected state in prepareForReuse of the cell:
- (void)prepareForReuse
{
self.selected = NO;
}
If the reused cell is selected, the collection view will set selected to "YES" after prepareForReuse is called.
This is something the UICollectionView should be doing on it's own. Thankfully the solution is simple. Unfortunately I spent a ton of time working around this bug by tracking my own select state. I didn't realize why it was happening until I was working on another project with smaller cells.
Also Try this
I'm not seeing why this would take place. I do not believe the issue is the use of row vs item, though you really should use item. I can imagine, though, if your collection view has more than one section, that only looking at row/item but ignoring section would be a problem (i.e. it would select the same item number in every section).
To cut the Gordian knot, I'd suggest saving the NSIndexPath of the selected item, and then using that for the basis of comparison. That also makes it easy to render an optimization in didSelectItemAtIndexPath. Anyway, first define your property:
#property (nonatomic, strong) NSIndexPath *selectedItemIndexPath;
And then implement cellForItemAtIndexPath and didSelectItemAtIndexPath:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"Cell";
CollectionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
cell.imageView.image = ...
if (self.selectedItemIndexPath != nil && [indexPath compare:self.selectedItemIndexPath] == NSOrderedSame) {
cell.imageView.layer.borderColor = [[UIColor redColor] CGColor];
cell.imageView.layer.borderWidth = 4.0;
} else {
cell.imageView.layer.borderColor = nil;
cell.imageView.layer.borderWidth = 0.0;
}
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
// always reload the selected cell, so we will add the border to that cell
NSMutableArray *indexPaths = [NSMutableArray arrayWithObject:indexPath];
if (self.selectedItemIndexPath)
{
// if we had a previously selected cell
if ([indexPath compare:self.selectedItemIndexPath] == NSOrderedSame)
{
// if it's the same as the one we just tapped on, then we're unselecting it
self.selectedItemIndexPath = nil;
}
else
{
// if it's different, then add that old one to our list of cells to reload, and
// save the currently selected indexPath
[indexPaths addObject:self.selectedItemIndexPath];
self.selectedItemIndexPath = indexPath;
}
}
else
{
// else, we didn't have previously selected cell, so we only need to save this indexPath for future reference
self.selectedItemIndexPath = indexPath;
}
// and now only reload only the cells that need updating
[collectionView reloadItemsAtIndexPaths:indexPaths];
}
Check also this
Your observation is correct. This behavior is happening due to the reuse of cells. But you dont have to do any thing with the prepareForReuse. Instead do your check in cellForItem and set the properties accordingly. Some thing like..
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"cvCell" forIndexPath:indexPath];
if (cell.selected) {
cell.backgroundColor = [UIColor blueColor]; // highlight selection
}
else
{
cell.backgroundColor = [UIColor redColor]; // Default color
}
return cell;
}
-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *datasetCell =[collectionView cellForItemAtIndexPath:indexPath];
datasetCell.backgroundColor = [UIColor blueColor]; // highlight selection
}
-(void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *datasetCell =[collectionView cellForItemAtIndexPath:indexPath];
datasetCell.backgroundColor = [UIColor redColor]; // Default color
}
I solved my issue;
The problem was very simple, I should have initialise MutableArray not in the Method didSelectItemAtIndexPath, but in the ViewDidLoad. Now it adding pictures one by one
I'm trying to create CollectionView similar to 500px app.
App logic: After app is launched, CollectionView loads empty cells(around 100), then when user scrolls down, cells are filled with image. At the end of view, new batch of empty cells are created(Load More). Images are pulled from Instagram, 20 per call.
Problem: When new images are pulled(async), cell rendering continues and usually it manages to create 3-5 empty cells before next batch of images are available.
Question: Is there any solutions for this problem? Maybe there's a better way to achieve this?
Code:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
ALPhotoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"photo" forIndexPath:indexPath];
cell.backgroundColor = [UIColor grayColor];
if (indexPath.row < [self.photos count]) {
cell.photo = self.photos[indexPath.row];
}
if (indexPath.row == [self.photos count]-6) {
[ALLoadMore loadMore:self.nextPage input:self.photos completionHandler:^(NSArray *newLoad, NSString *next) {
self.photos = newLoad;
self.nextPage = next;
NSLog(#"%#", self.nextPage);
dispatch_async(dispatch_get_main_queue(), ^{
//[self.collectionView reloadData];
if ([self.photos count] > indexPath.row) {
cell.photo = self.photos[indexPath.row];
NSLog(#"call");
}
NSLog(#"%lu", (unsigned long)[self.photos count]);
});
}];
}
return cell;}
For now cell count is a static number:120
I have a probem when trying to reload a UICollectionview in iOS.
I am using it to display levels in a game.
The collectionview consists of 10 cells. The content of the cells depends if a level is unlocked. If the level is unlocked the cell displays the level (a custom UIView), else it displays an image.
I had to create individual cell identifiers for this to work, and everything displays perfectly on load.
My problem is when a user is playing an unlocked level and then unlocks the next level. When the user navigates back from the game view to the level selection view, the cells are not reloaded correctly (just shows up empty where the custom views should be, the images display correctly).
I have tried to unload the array with levels in viewWillAppear and then calling [collectionview reloadData];, then loading the levels and reloading the collectionview again, but that does not help.
How can I empty the entire collectionview and recreate the cell identifiers when the view is displayed?
Thanks
-EDIT! UPDATED WITH CODE -
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:YES];
levelsArray = nil;
puzzlesArray = nil;
levelsArray = [[NSMutableArray alloc]init];
puzzlesArray = [[NSMutableArray alloc]init];
[collectionView reloadData];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *data = [defaults objectForKey:#"puzzles"];
puzzlesArray = [NSKeyedUnarchiver unarchiveObjectWithData:data];
if ([puzzlesArray count] == 0) {
[self.navigationController popViewControllerAnimated:YES];
}
else {
NSLog(#"%i puzzles loaded", [puzzlesArray count]);
//Get alle puzzles for the current category
for (Puzzle *puzzle in puzzlesArray) {
if ([[[NSUserDefaults standardUserDefaults]objectForKey:#"Category"] isEqualToString:[puzzle categoryName]]) {
[levelsArray addObject:puzzle];
}
}
}
NSLog(#"View will appear");
[collectionView reloadData];
}
And in the cell for item at index path
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath; {
BOOL isUnlocked = [self isPuzzleUnlocked:[indexPath row]];
if ([[NSUserDefaults standardUserDefaults]boolForKey:#"u"] == YES) {
isUnlocked = YES;
}
[self.collectionView registerNib:[UINib nibWithNibName:#"CVCell" bundle:nil] forCellWithReuseIdentifier:[NSString stringWithFormat:#"%#%d", kCellReuseIdentifier, indexPath.row]];
CVCell *cell = (CVCell *)[collectionView dequeueReusableCellWithReuseIdentifier:[NSString stringWithFormat:#"%#%d", kCellReuseIdentifier, indexPath.row] forIndexPath:indexPath];
[cell setPuzzleInfo:[levelsArray objectAtIndex:[indexPath row]] unlocked:isUnlocked];
return cell;
}
In line with Rob's suggestion.
-(void)viewWillAppear {
[super viewWillAppear];
[self.collectionView reloadData];
}
In cellForItemAtIndexPath you should simply check if the item is unlocked or not and display the proper image or custom cell.
That's all. It is definitely not necessary to recreate the collection view.
You might want to honour the MVC (model-view-controller) design pattern by giving both your game controller and the level controller access to the data that models the levels. When what is happening in the game unlocks a new level, it should change this data. The collection view should reload itself based on this data just before it appears.
EDIT:
If you get the same cells/items as before even though you reloadData, this means that your configuration is not set up correctly. The key is to set both states of unlocked explicitly - reloadData will then update the item if it has changed.
EDIT2:
Both your itemForRowAtIndexPath methods are messed up. The call to registerNib does not belong there at all! It is supposed to be called during the initialization process. If you use storyboard, it is not necessary at all.
Here is a simple framework that you should use:
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv
cellForItemAtIndexPath:(NSIndexPath *)indexPath; {
PuzzleInfo *puzzleObject = [dataArray objectAtIndex:indexPath.row];
// you can use your more complicated data structure here, but
// have one object, dictionary, array item, etc. that has the info you need
NSString *identifier = puzzleObject.unlocked ? kUnlockedCell : kLockedCell;
CVCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier
forIndexPath:indexPath];
if (puzzleObject.unlocked) {
// configure the unlocked cell
}
else {
// configure the locked cell
}
return cell;
}
I fixed it!
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath; {
NSLog(#"CALLED");
BOOL isUnlocked = [self isPuzzleUnlocked:[indexPath row]];
if ([[NSUserDefaults standardUserDefaults]boolForKey:#"u"] == YES) {
isUnlocked = YES;
}
int rand = arc4random() % 99001;
[self.collectionView registerNib:[UINib nibWithNibName:#"CVCell" bundle:nil] forCellWithReuseIdentifier:[NSString stringWithFormat:#"%#%d%i", kCellReuseIdentifier, indexPath.row, rand]];
CVCell *cell = (CVCell *)[collectionView dequeueReusableCellWithReuseIdentifier:[NSString stringWithFormat:#"%#%d%i", kCellReuseIdentifier, indexPath.row, rand] forIndexPath:indexPath];
[cell setPuzzleInfo:[levelsArray objectAtIndex:[indexPath row]] unlocked:isUnlocked];
return cell;
}
Not a very memory effecient solution (or elegant), but it works. Adding a random number to each cell forces it to be recreated each time. As long as I only have 10 cells in a collectionview I think it will be ok..
Thanks for all your help :]