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]
Related
My app needs to display huge number of images(about 2000) in a UITableView. Basically, I use the following code to construct my UITableViewCell:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"Cell"];
}
// Some Operations...
NSString *path = [self.dataArray jk_objectWithIndex:indexPath.row];
UIImage *img = [UIImage imageWithContentsOfFile:path];
cell.imageView.image = img;
return cell;
}
This works but when the tableview loads, memory increase fast and it seems that all the images is loaded to the memory.
Is there any good ideas to deal with this? I just want to save the memory.
BTW, anyone knows what the common way is to achieve this need? I think loading all the images to memory is the stupidest way... And the code I initial rows of tableview is the following:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (!_isLoading) {
return self.dataArray.count; // about 2000... That's terrible
} else {
return 0;
}
}
Thanks!
There are two problems with your code.
First, and most important, the images are large but the display of the images in the table is small. That's wrong. You should load the image only at the size you actually need for display.
Second, images are cached by default. You need to prevent the caching of these images as they are loaded.
You can easily do both of those things in your cellForRowAt by using the ImageIO framework.
I figure out a way to this question. Add these lines of code to cellForRowAtIndexPath:
CGRect rectInTableView = [tableView rectForRowAtIndexPath:indexPath];
CGRect rectInSuperview = [tableView convertRect:rectInTableView toView:[tableView superview]];
if ( rectInSuperview.origin.y > SCREEN_HEIGHT || rectInSuperview.origin.y + rectInSuperview.size.height < 0 ) {
cell.imageView.image = self.placeholder;
} else {
NSString *path = [self.dataArray jk_objectWithIndex:indexPath.row];
UIImage *img = [UIImage imageWithContentsOfFile:path];
cell.imageView.image = img;
}
First I check whether the cell is shown on the screen. If yes, the imageView show my data images. If not, it show the placeholder instead. Also, the placeholder is init with [UIImage imageNamed:]. This is the best way because it will be used frequently.
I have 4 UICollectionViewCell in which I want to make first 2 cell to have image in it mandatory. Sample code is as below. Below is a usecase:
There are 4 UICollectionViewCell if I click on each cell will open camera and we can either take a photo or select image from image gallery. How to know first 2 cell must have image. Looking for condition to check this logic.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
Image *image = (Image *)self.defect.imageSet[indexPath.item];
ADRPhotoCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([ADRPhotoCollectionViewCell class]) forIndexPath:indexPath];
cell.image = image.thumbnailImage ? image.thumbnailImage : nil;
cell.photoType = [self.defect defectPhotoTypeForIndex:indexPath];
return cell;
}
#pragma mark - UICollectionViewDelegate
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
Image *image = self.defect.imageSet[indexPath.item];
if (self.selectedImage != image) {
[self updateAnnotatedImage];
self.selectedImage = image;
[self.jotController clearAll];
[self updateUserInterface];
if (image.thumbnailImage) {
[self transitionImage:YES];
} else {
self.imageView.image = nil;
[self showCameraPicker];
}
} else if (!image.thumbnailImage) {
//clicked the + to add a new image
[self showCameraPicker];
}
}
You can check if there is an image in UIImageView or not by this way:
-(BOOL) checkImages {
for (int i=0, i<2 , i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
ADRPhotoCollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
if (cell.imageView.image == nil || CGSizeEqualToSize(cell.imageView.image.size, CGSizeZero)) {
//there isn't any image or the image is empty
return NO;
}
}
return YES;
}
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.
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'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