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
Related
I am trying to change UICollectionViewCell size depending on a slider's value. Right now, I am managing this by calling reloadData on my UICollectionView each time my slider has its value changed. The problem is that with big data sources, the refresh is not smooth and sometimes free the application for a little time. Is there any way to enhance this ? I specify that I have images in my cells. Here is the code I wrote :
- (IBAction)didChangeCellSize:(UISlider *)sender
{
[self.collectionView reloadData];
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
float size = 120.0 * (self.cellSizeSlider.value + 1);
return CGSizeMake(size, size);
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
ProductCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"productCollectionViewCell" forIndexPath:indexPath];
if ((self.filteredProducts == nil && self.products.count > 0 && indexPath.row < self.products.count) || (self.filteredProducts && self.filteredProducts.count > 0 && indexPath.row < self.filteredProducts.count))
{
NSDictionary *product;
NSData *imageData;
if (self.filteredProducts)
{
product = [self.filteredProducts objectAtIndex:indexPath.row];
}
else
{
product = [self.products objectAtIndex:indexPath.row];
}
imageData = product[ParseDataManagerItemImageData];
if (imageData)
{
UIImage *image = [UIImage imageWithData:imageData];
if (image)
{
cell.productImageView.image = image;
}
else
{
cell.productImageView.image = [UIImage imageNamed:#"DefaultCartItem"];
}
}
else
{
cell.productImageView.image = [UIImage imageNamed:#"DefaultCartItem"];
}
if (self.editMode)
{
cell.deleteButton.hidden = NO;
}
else
{
cell.deleteButton.hidden = YES;
}
cell.productNameLabel.text = [product[DataManagerItemTitle] isKindOfClass:[NSString class]] ? product[DataManagerItemTitle] : #"";
cell.indexPath = indexPath;
cell.productsVC = self;
}
return cell;
}
As confirmed in the question comment, loading image is the cause of the problem.
Allocating an image array first, can solve the problem if products.count is relative small. Or you will need some kind of 'smarter' cache service running in the background to serve the correct images needed on screen, because I saw you're also using a filter.
Loading the images in a separate thread should help in both cases, so that you can determine whether to load extra files from data or from existing cached versions, maybe an array or other kind, and throw away unused images if your cells can be modified later.
I think you don't need to reload all the cells. Only reload the cells which are visible on screen or currently active. Try this if its work.
NSArray *visibleCellIndexPaths = [collectionView indexPathsForVisibleItems];
[collectionView reloadItemsAtIndexPaths:visibleCellIndexPaths]
I have this strange issue where my collection view is loading data from a plist file and its perfectly fine on iOS7, but not in iOS8.
My cells are only containing a UILabel that shows a specific item from a plist file. It's really just a list of strings. It works if I use it, the cells react properly and all is good, expect one thing :
The labels don't show up. If I tap it I have the data and the program runs fine.
If I browse down and up again, they reload with the proper labels.
It's really just the very first loading that does not show the labels.
I've tried putting a reloadData button to be sure it's called as late as possible, but to no avail.
Any clue?
Here is some code :
Collection view methods
- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath{
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
cell.layer.borderColor = ClearColor.CGColor;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
static NSString *cellIdentifier = #"TagCell";
UICollectionViewCell *cell = [_cvTags dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
if (cell == nil) {
cell = [[UICollectionViewCell alloc]init];
[cell setRestorationIdentifier:cellIdentifier];
}
UILabel *lbT = (UILabel*)[cell viewWithTag:1];
if (score == 1){
lbT.text = [[tags objectForKey:PLIST_Plus]valueForKey:[NSString stringWithFormat:#"%i",indexPath.row]];
}
if (score == -1){
lbT.text = [[tags objectForKey:PLIST_Minus]valueForKey:[NSString stringWithFormat:#"%i",indexPath.row]];
}
cell.contentView.backgroundColor = ClearColor;
cell.layer.borderColor = ClearColor.CGColor;
cell.layer.borderWidth = 3;
cell.layer.cornerRadius = 20;
return cell;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
if (score == 1){
return [[tags objectForKey:PLIST_Plus] count];
}
if (score == -1){
return [[tags objectForKey:PLIST_Minus] count];
}
else{
return 0;
}
}
viewDidLoad
tags = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"tags" ofType:#"plist"]];
plist File
It's just an XML file with a dictionary.
Note :
It's really just fine on iOS7.
I have the exact same issue with a tableview on another view. But its worst. I'm filling the cells with hardcoded labels INSIDE the cellForRow method, and they still don't show until I reuse them.
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
My problem is that the ProgressView only shows up in the first 2 cells. What is wrong with my code?
Note: My CollectionView scrolls horizontally and each cell covers the whole screen. I think this might have something to do since I tried showing all cells in the same view and they work fine. All ProgressViews show.
EDITED CODE:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"Cell";
VestimentaDetailCell *cell = (VestimentaDetailCell *) [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
cell.progressView.hidden = NO;
[cell.progressView setProgress:0.02];
PFFile *storeLooks = [self.vestimenta objectForKey:[NSString stringWithFormat:#"image_%ld", (long)indexPath.item]];
NSMutableString *precio = [NSMutableString string];
for (NSString* precios in [self.vestimenta objectForKey:[NSString stringWithFormat:#"precios_%ld", (long)indexPath.item]]) {
[precio appendFormat:#"%#\n", precios];}
[storeLooks getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error && data.length > 0) {
cell.imageFile.image = [UIImage imageWithData:data];
} else {
cell.progressView.hidden = YES;
}
} progressBlock:^(int percentDone) {
float percent = percentDone * 0.02;
[cell.progressView setProgress:percent];
if (percentDone == 100){
cell.progressView.hidden = YES;
} else {
cell.progressView.hidden = NO;
}
}];
return cell;
}
Instead of removing the progress view from the cell, you should simply setHidden:YES.
This way when the cells are reused, the progress view will be present and you can then setHidden:NO when you want to start loading stuff in that cell.
Also be careful with progress blocks inside your cells when they are reused. Remember to either cancel the loading operation if the cell is reused, or make sure the progress block only continues updating it's cell if the cell hasn't been reused for a different data item.
So for example I would set the progress to 0. where you're showing the progress view like so:
VestimentaDetailCell *cell = (VestimentaDetailCell *) [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
cell.progressView.hidden = NO;
[cell.progressView setProgress:0.];
And because you're hiding the progressView when the data is finished loading, I would just get rid of this code in the progressBlock:
if (percentDone == 100){
cell.progressView.hidden = YES;
} else {
cell.progressView.hidden = NO;
}
I think this is related to the cells being reused as they come on screen. Since you are removing the progressView from the cell [cell.progressView removeFromSuperview] , once it is dequeued, it is still missing. You could try overriding the prepareForReuse method in your VestimentaDetailCell class to add it back so that all dequeued cells are brought back to their original state.
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 :]