I am trying to asynchronously load an image in a UITableVIew.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = nil;
NSString *cellIndetify = [NSString stringWithFormat:#"cell%d",tableView.tag -TABLEVIEWTAG];
cell = [tableView dequeueReusableCellWithIdentifier:cellIndetify];
IndexPath *_indextPath = [IndexPath initWithRow:indexPath.row withColumn:tableView.tag - TABLEVIEWTAG];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIndetify];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.contentView.backgroundColor = [UIColor blackColor];
// here I init cell image view
UIView *cellSub = [self.waterFlowViewDatasource waterFlowView:self cellForRowAtIndexPath:_indextPath];
cellSub.backgroundColor = [UIColor blackColor];
[cell.contentView addSubview:cellSub];
cellSub.tag = CELLSUBVIEWTAG;
}
// fill image info
[self.waterFlowViewDatasource waterFlowView:self fillDataIntoCellSubview:[cell viewWithTag:CELLSUBVIEWTAG] withIndexPath:_indextPath];
CGFloat cellHeight = [self.waterFlowViewDelegate waterFlowView:self heightForRowAtIndexPath:_indextPath];
CGRect cellRect = CGRectMake(0, 0, _cellWidth, cellHeight);
[[cell viewWithTag:CELLSUBVIEWTAG] setFrame:cellRect];
[self.waterFlowViewDatasource waterFlowView:self relayoutCellSubview:[cell viewWithTag:CELLSUBVIEWTAG] withIndexPath:_indextPath];
return cell;
}
in that ImageView class I init:
(id)initWithIdentifier:(NSString *)indentifier
{
if(self = [super initWithIdentifier:indentifier])
{
self.backgroundColor = [UIColor blackColor];
if (!self.imageView) {
_imageView = [[UIImageView alloc] init];
self.imageView.backgroundColor = IMAGEVIEWBG;
[self addSubview:self.imageView];
self.imageView.layer.borderWidth = 1;
self.imageView.layer.borderColor = [[UIColor colorWithRed:0.85 green:0.85 blue:0.85 alpha:1.0] CGColor];
}
......some others controllers
}
-(void)setImageWithURL:(NSURL *)imageUrl{
UIImage *cachedImage = [[SDImageCache sharedImageCache] imageFromKey:[imageUrl absoluteString]];
if (cachedImage) {
self.imageView.image = cachedImage;
}else{
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadWithURL:imageUrl delegate:self options:SDWebImageCacheMemoryOnly userInfo:[[NSDictionary alloc] initWithObjectsAndKeys:[imageUrl absoluteString], #"image_url", [NSValue valueWithBytes:&imageSize objCType:#encode(CGSize)], #"image_size", [NSNumber numberWithInt:arrIndex], #"imageview_index", nil]];
_imageView.image = [UIImage imageNamed:#"album_detail_placeholder.png"];
}
.....
.....
- (void)imageDecoder:(SDWebImageDecoder *)decoder didFinishDecodingImage:(UIImage *)image userInfo:(NSDictionary *)userInfo {
imageView.image = image
}
As you see here I used SDWebImageManager, but that doesn't seem to be the problem.
When pull up to next page, before the next page's images have loaded, if I scroll back to previous page's loaded image, the image will be covered by another image (which is should display in the newest page, not current page...) (for example, in the first page, I have a image and there is cat in this image, and then I scroll down till pull up to request next page, and after get all next page image's url, start asynchronous thread download images, before the newest images downloaded, scroll back to top, maybe first page. After awhile, some images downloaded, they should display in there cell, but now the images will cover the current page images, maybe the cat image now have a dog image on it) what shall I do?
When a row is scrolled off-screen, the table view reuses that row's cell for another row that has scrolled on-screen.
The problem is that you are using the ImageView as the delegate of the background loader, and the ImageView is associated with a cell, not a row. By the time the background loader finishes loading the image, the cell may have been reused for another row.
You need to make the table view's data source be the delegate for the background image loader. It should put the index path of the row into the userInfo dictionary when it calls downloadWithURL:delegate:options:userInfo:. When the downloader sends imageDecoder:didFinishDecodingImage:userInfo: to the data source, the data source should get the index path out of the userInfo and use it to ask the table view for the cell currently displaying that row (by sending cellForRowAtIndexPath: to the table view).
Related
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 7 years ago.
Improve this question
I am working with an IOS application. In my project have several UITableView with many rows, and each row has two images. When I scroll fast then It can't load cells smoothly. How can I scroll smoothly ???
N.B: I don't want to load all rows at a time.
Please Help
Edited:
Here is my code :
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell;
float sca=appDelegate.deviceScaleFloat;
float XOffset=0*sca;
cell = [[UITableViewCell alloc] init];
cell.opaque = YES;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
UIImage *backImg;
backImg= [UIImage imageNamed:[NSString stringWithFormat:#"background-goal-collect%#.png",ipadExtention]];
backImg=[self scaleWithScaleFactor:backImg];
UIImageView *btnBuyImageView=[[UIImageView alloc]initWithImage:backImg];
btnBuyImageView.frame=CGRectMake(XOffset, 0, backImg.size.width, backImg.size.height);
[cell.contentView addSubview: btnBuyImageView];
for (int i=0; i<5 && indexPath.row*5+i<[catFishes count]; i++) {
int productIndex = (int)indexPath.row*5 + i;
DBProductAttributes *productAttributes = [allProductAttributes objectAtIndex:productIndex];
DBProductInfo *productInfo = [catFishes objectAtIndex:productIndex];
UIImage *frameImage = [UIImage imageNamed:[NSString stringWithFormat:#"background-element-%d%#.png",productAttributes.elementid,ipadExtention]];
frameImage=[self scaleWithScaleFactor:frameImage];UIImageView *frameView = [[UIImageView alloc] initWithImage:frameImage];
frameView.frame = CGRectMake((frameImage.size.width*i)+10*sca+5*i*sca, 5*sca , frameImage.size.width, frameImage.size.height);
frameView.userInteractionEnabled=YES;
[cell.contentView addSubview:frameView];
MyTapGestureRecognizer *tapGesture=[[MyTapGestureRecognizer alloc] init];
tapGesture.tag=productIndex;
[tapGesture addTarget:self action:#selector(buttonClicked:)];
[frameView addGestureRecognizer:tapGesture];
NSString *IconStr = [NSString stringWithFormat:#"i%db.png", productInfo.productid];
UIImage *btnImg = [UIImage imageNamed:IconStr];
if(![self isProductPurchesed:productInfo.productid])
{
if([ITIWAppDelegate blackimageforstore]>0)
{
btnImg = [self getBlackAndWhiteVersionOfImage:btnImg];
}
}
UIImageView *imageIconView;
imageIconView = [[UIImageView alloc] initWithImage:btnImg];
imageIconView.frame = CGRectMake(frameView.frame.origin.x+frameImage.size.width-64*sca, frameView.frame.origin.y/*+frameImage.size.height-64*sca*/ , 64*sca, 64*sca);
imageIconView.opaque = YES;
[cell.contentView addSubview:imageIconView];
UILabel *name;
name = [[UILabel alloc] initWithFrame:CGRectMake(frameView.frame.origin.x, frameView.frame.origin.y+62*sca, frameImage.size.width-0*sca, 18.0f*sca)];
name.text = productInfo.product_name;
name.font = [UIFont fontWithName:#"Georgia" size:12.0f*sca];
name.adjustsFontSizeToFitWidth = YES;
name.textAlignment = NSTextAlignmentCenter;
if(![self isProductPurchesed:productInfo.productid])
name.backgroundColor=[UIColor grayColor];
else
name.backgroundColor = [UIColor colorWithRed:colorCodeDragonBook[productAttributes.elementid-1][0]/255.0f green:colorCodeDragonBook[productAttributes.elementid-1][1]/255.0f blue:colorCodeDragonBook[productAttributes.elementid-1][2]/255.0f alpha:1.0f];
name.textColor = [UIColor colorWithWhite:1.0 alpha:1.0];
//name.shadowColor = [UIColor blackColor];
name.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
[cell.contentView addSubview:name];
NSArray *otherElements = [productAttributes.otherElementid componentsSeparatedByString:#","];
int k=0;
UIImage *habitatFlag = [UIImage imageNamed:[NSString stringWithFormat:#"flag-%d.png",productAttributes.elementid]]; UIImageView *habitatFlagView = [[UIImageView alloc] initWithImage:habitatFlag];
habitatFlagView.frame = CGRectMake(frameView.frame.origin.x-1*sca, frameView.frame.origin.y-1*sca , 15*sca, 22*sca);
[cell.contentView addSubview:habitatFlagView];
k+=15;
for (int i=0; i<[otherElements count]; i++) {
int otherElementid = [[otherElements objectAtIndex:i] intValue];
if(otherElementid==productAttributes.elementid) continue;
UIImage *habitatFlag = [UIImage imageNamed:[NSString stringWithFormat:#"flag-%d.png",otherElementid]]; UIImageView *habitatFlagView = [[UIImageView alloc] initWithImage:habitatFlag];
habitatFlagView.frame = CGRectMake(frameView.frame.origin.x+k*sca-1*sca, frameView.frame.origin.y-1*sca , 15*sca, 22*sca);
[cell.contentView addSubview:habitatFlagView];
k+=15;
}
}
return cell;
}
Problem is occurring when the cells are going to off screen, the tableview release all cells of off screen. And when after that I want to scroll the cells are reloading. I think it is not optimal to load cells. But I don't know how to optimize this.
the best you have to do is load the images asynchronously, and not in the main thread.
If you want, you can use my ImageLoader project : https://github.com/celian-m/ImageLoader/blob/master/ImageLoader.swift
All you have to do is using CMImageView instead of UIImageView.
Then you can do [myImageView assignImageFromUrl:YOUR_URL]
This will load your images in the background thread, in FIFO mode, and save it in memory and disk cache ( i.e. : you need to load each image only 1 time ).
You can make use of SDWebImageCache so that it'll cache the images in disk and the loading of images becomes much faster.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0ul);
dispatch_async(queue, ^{
//set image here
});
What about setting the image async in the tableview callback:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
May I ask whether the images are web images or local images?
In my iOS app, I have a UICollectionView where each cell contains an image. To prevent the view from taking a long time to load, I load each with a blank image and title before loading the image contents in a background task.
I logged which images are getting loaded through in the background async task, and it seems like the images of cells off screen get loaded first, followed by the cells at the top of the screen. This makes the app seem unresponsive, and I'd rather have the cells at the top take priority in terms of loading:
I also notice that once I start scrolling, the images in the cells suddenly start appearing, but they take much longer to appear on their own. Can anyone suggest strategies to control the ordering that UICollectionCells load in?
Here is my code:
Iterate over projects and add imageViews to an NSMutableArray projectContainers, which then gets turned into cells
for (NSDictionary *currentProject in projects)
{
// data entry
[projectIDs addObject: [currentProject objectForKey:#"id"]];
NSString *projectTitle = [currentProject objectForKey:#"title"];
id delegate = [[UIApplication sharedApplication] delegate];
self.managedObjectContext = [delegate managedObjectContext];
CustomLabel *cellLabel=[[CustomLabel alloc]init];
cellLabel.text = trimmedProjectTitle;
[titles addObject:projectTitle];
CGSize maxLabelSize = CGSizeMake(cellWidth,100);
CustomLabel *titleLabel = [[CustomLabel alloc]init];
// titleLabel styling
titleLabel.backgroundColor = [[UIColor blackColor]colorWithAlphaComponent:0.5f];
titleLabel.textColor =[UIColor whiteColor];
[titleLabel setFont: [UIFont fontWithName: #"HelveticaNeue" size:12]];
titleLabel.text = trimmedProjectTitle;
CGSize expectedLabelSize = [titleLabel.text sizeWithFont:titleLabel.font constrainedToSize:maxLabelSize lineBreakMode:NSLineBreakByWordWrapping];
CGRect labelFrame = (CGRectMake(0, 0, cellWidth, 0));
labelFrame.origin.x = 0;
labelFrame.origin.y = screenWidth/2 - 80 - expectedLabelSize.height;
labelFrame.size.height = expectedLabelSize.height+10;
titleLabel.frame = labelFrame;
// add placeholder image with textlabel
UIImageView *imagePreview = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, cellWidth, cellHeight)];
imagePreview.contentMode= UIViewContentModeScaleAspectFill;
imagePreview.clipsToBounds = YES;
[imagePreview setImage:[UIImage imageNamed:#"blank.png"]];
[imagePreview addSubview:titleLabel];
[imagePreview.subviews[0] setClipsToBounds:YES];
[projectContainers addObject: imagePreview];
// add project thumbnail images in async
dispatch_async(bgQueue, ^{
NSDictionary *imagePath = [currentProject objectForKey:#"image_path"];
NSString *imageUrlString = [imagePath objectForKey: #"preview"];
NSURL *imageUrl = [NSURL URLWithString: imageUrlString];
NSData *imageData = [[NSData alloc] initWithContentsOfURL:(imageUrl)];
UIImage *image = [[UIImage alloc] initWithData:(imageData)];
if(image){
NSLog(#"project with image: %#", projectTitle);
[imagePreview setImage: image];
}
BOOL *builtVal = [[currentProject objectForKey:#"built"]boolValue];
if(builtVal){
UIImageView *builtBanner =[[UIImageView alloc]initWithImage:[UIImage imageNamed:#"built_icon.png"]];
builtBanner.frame = CGRectMake(screenWidth/2 -80, 0, 50, 50);
[imagePreview addSubview: builtBanner];
}
});
}
renders cells using the NSMutableArray projectContainers:
-(UICollectionViewCell*)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
// NSLog(#"cellForItemAtIndexPath");
static NSString *identifier = #"NewCell";
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
if(!reloadProjects){
UIImageView *preview = (UIImageView*) [cell.contentView viewWithTag:[[projectIDs objectAtIndex:indexPath.row]intValue]];
UIImageView *previewContent = [projectContainers objectAtIndex:indexPath.row];
// NSLog(#"fetching image tag %d", [[projectIDs objectAtIndex:indexPath.row]intValue]);
if (!preview)
{
previewContent.tag = [[projectIDs objectAtIndex:indexPath.row]intValue];
// NSLog(#"creating previewContent %li", (long) previewContent.tag);
[cell addSubview: previewContent];
}
[self.collectionView setBackgroundColor:collectionGrey];
cell.contentView.layer.backgroundColor = [UIColor whiteColor].CGColor;
return cell;
}
return cell;
}
EDIT: Working Solution
Thanks to rob mayoff for helping me come out with a solution. This is what I ended up doing, which loads the images much faster:
// add project thumbnail images in async
dispatch_async(imageQueue, ^{
NSDictionary *imagePath = [currentProject objectForKey:#"image_path"];
NSString *imageUrlString = [imagePath objectForKey: #"preview"];
NSURL *imageUrl = [NSURL URLWithString: imageUrlString];
NSData *imageData = [[NSData alloc] initWithContentsOfURL:(imageUrl)];
UIImage *image = [[UIImage alloc] initWithData:(imageData)];
if(image){
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"project with image: %#", projectTitle);
[imagePreview setImage: image];
});
}
BOOL *builtVal = [[currentProject objectForKey:#"built"]boolValue];
if(builtVal){
UIImageView *builtBanner =[[UIImageView alloc]initWithImage:[UIImage imageNamed:#"built_icon.png"]];
builtBanner.frame = CGRectMake(screenWidth/2 -80, 0, 50, 50);
dispatch_async(dispatch_get_main_queue(), ^{
[imagePreview addSubview: builtBanner];
});
}
});
There are several things that code be improved in your code, but your chief complaint (“once I start scrolling, the images in the cells suddenly start appearing, but they take much longer to appear on their own”) is because you violated the commandment:
Thou shalt only access
thy view hierarchy
from the main thread.
Look at your code:
dispatch_async(bgQueue, ^{
...
[imagePreview addSubview: builtBanner];
You're manipulating the view hierarchy from a background thread. This is not allowed. For example, see the note at the bottom of this page, or the “Threading Considerations” in the UIView Class Reference.
You need to dispatch back to the main thread to update the view hierarchy.
Watch the Session 211 - Building Concurrent User Interfaces on iOS video from WWDC 2012. It talks in depth about how to do what you're trying to do, efficiently. See also this answer.
I have added this UICollectionViewDelegate with my implementation:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
SAPCollectionViewCell *collectionViewCell = (SAPCollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:#"SAPCollectionViewCell" forIndexPath:indexPath];
NSInteger index = indexPath.item + indexPath.section * 2;
NSString *imagePath = [_images objectAtIndex:index];
collectionViewCell.index = index;
[collectionViewCell setImageWithPath:imagePath];
collectionViewCell.delegateCell = self;
return collectionViewCell;
}
In my custom cell I have this method:
- (void)setImageWithPath:(NSString *)imagePath
{
if (!self.imageView)
{
CGRect rect = self.contentView.frame;
self.imageView = [[UIImageView alloc] initWithFrame:rect];
[self.contentView addSubview:self.imageView];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage *image = [UIImage imageFromPath:imagePath];
UIImage *resizedImage = [UIImage imageWithImage:image scaledToWidth:rect.size.width];
dispatch_async(dispatch_get_main_queue(), ^{
[self.imageView setImage:resizedImage];
});
});
}
}
So as you can see if the cell does not have UIImageView then I start to create it and in background I get image from local path and resize it to cell content view width, then in main queue I set image to UIImageView.
This part work good but when I try to scroll my UICollectionView e.g. with 10 images I noticed that for example if first 2 top images were disappeared when I scroll down and then when I scroll back to the top, the images are changed placed.
This is state of images before scrolling down:
And this state after I scroll UIColectionView back to the top:
So as you can see first to item changed theirs location. As I have set if (!self.imageView) in my code above it should means that the image never will be created twice or more times.
I opened debugger and was checking this method:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
So the images paths returned one by one as it worked when UICollectionView just created cells at first time.
At first time first UICollectionViewCell has 0x8fbc1d0
and second has 0x8d0a4c0 address
But when I scroll down and then scroll up the debugger shown me second address at first 0x8d0a4c0 and then shown me 0x8fbc1d0
But I can't understand why items change theirs order. Or maybe here is another issue?
Also I don't have any method in the code that make for example for me reorder for cell. just few UICollection view methods delegate that configure count of cells and create them.
Also if I remove if (!self.imageView) seems everything works good, but then my code every time invoke dispatch that's no good for me, because when I resize image to the cell size I don't need do it twice.
When you scroll the two items off screen, your CollectionView puts the associated cells back onto the 'queue'. When you scroll back up again, the dequeueResuableCellwithReuseIdentifier in cellForItemAtIndexPath retrieves them from that queue, but (as you've seen) not necessarily in the same order. Thus the cell which was item 0 has been used for item 1 and vice versa. As you've seen, the if (!self.imageView) actually stops the correct image from being loaded. If you want to avoid retrieving and resizing them each time, then I would store the (resized) imageViews in an array.
- (void)setImageWithPath:(NSString *)imagePath
{
if (!self.imageView)
{
CGRect rect = self.contentView.frame;
self.imageView = [[UIImageView alloc] initWithFrame:rect];
[self.contentView addSubview:self.imageView];
}
if (![self.imagePath isEqualsToString:imagePath] && self.imageView.image == nil)
{
self.imagePath = imagePath;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage *image = [UIImage imageFromPath:imagePath];
UIImage *resizedImage = [UIImage imageWithImage:image scaledToWidth:rect.size.width];
dispatch_async(dispatch_get_main_queue(), ^{
if ([self.imagePath isEqualsToString:imagePath])
{
[self.imageView setImage:resizedImage];
}
});
});
}
}
In my ipad app, i has 200 images and i add these images into an array.
Then add this array into image view by looping.
Then, i add this image view as sub view of scroll view.
When i open app, my app is crash.
I try with reducing image size. But, it didn't work.
One of my friend told firstly i should add only image1 and image2.
When user scroll image1, then it show image2.
After that image1 is remove from image view.
And add image3 to image view.
He told it can maintain memory usage.
But, i don't know how can i do that? :D
Please, give me some example.
Thanks in advance.
My code is here,
- (void)viewDidLoad
{
[super loadView];
self.view.backgroundColor = [UIColor grayColor];
UIScrollView *ScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 44, self.view.frame.size.width, self.view.frame.size.height)];
ScrollView.pagingEnabled = YES;
// Create a UIImage to hold Info.png
UIImage *image1 = [UIImage imageNamed:#"Image-001.jpg"];
UIImage *image2 = [UIImage imageNamed:#"Image-002.jpg"];
UIImage *image200 = [UIImage imageNamed:#"Image-200.jpg"];
NSArray *images = [[NSArray alloc] initWithObjects:image1,image2,...,image200,nil];
NSInteger numberOfViews = 200;
for (int i = 0; i < numberOfViews; i++)
{
CGFloat xOrigin = i * self.view.frame.size.width;
UIImageView *ImageView = [[UIImageView alloc] initWithFrame:CGRectMake(xOrigin, 0, self.view.frame.size.width, self.view.frame.size.height-44)];
[ImageView setImage:[images objectAtIndex:i]];
[ScrollView addSubview:ImageView];
}
ScrollView.contentSize = CGSizeMake(self.view.frame.size.width * numberOfViews, self.view.frame.size.height);
[self.view addSubview:ScrollView];
It looks like you're accessing the images array at indexes 0..200, but it contains only three elements. Either set numberOfViews = 3;, or index into the array like this:
ImageView.image = images[i%3];
The modulo index will cause you to add each of the three images in a repeating sequence and still let you test with a larger number of scrollView subviews. I doubt that your crash is about memory with only 200 images, unless they are super-huge ones.
You should use a table view to display your images. You don't want to create an array of all those images, because that puts all those images into memory -- not a good thing. You don't even need to create an array to hold the names, since they have names you can easily construct from an integer (which you can get from indexPath.row in the tableView:cellForRowAtIndexPath: method). Something like this:
- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:#"Cell"];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 200;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
NSString *imageName = [NSString stringWithFormat:#"Image-%.3d.jpg",indexPath.row +1];
UIImage *image = [UIImage imageNamed:imageName];
cell.imageView.image = image;
return cell;
}
This example just uses the cell's default imageView to show the images, which is pretty small. You would probably want to create a custom cell with a larger image view in it (or a couple side by side if you want more than one image per row).
I have a system which loads alot of large images from the web and displays them in custom table cells. On older devices the memory warnings happen pretty quickly so I implemented a system of deleting some from the table to try to combat this but it didn't work well enough (lots of images were deleted affecting the UI).
So I thought I could load all the images into the device's cache and then load them from there - I've implemented SDWebImage. This is great but I still havent solved the problem of memory allocation as the images are still being displayed all the time and therefore kept in memory - causing crashes.
I think I need to implement a system which shows the images (from the cache) if the cell is being displayed and hide it if the cell is not showing - I'm just stuck at how to build such a system.
Or is this not going to work? Can you really keep the apps memory low (and stop it having memory warnings / crashing) by removing images from its table cells? Or do I just need to carry on with my earlier solution and just delete images/cells until the memory warnings stop?
Updated with code
TableViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0)
{
currentIndexPath = indexPath;
ImageTableCell *cell = (ImageTableCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
ImageDownloader *download = [totalDownloads objectAtIndex:[indexPath row]];
if (cell == nil)
{
cell = [[[ImageTableCell alloc] initWithStyle: UITableViewCellStyleDefault reuseIdentifier: CellIdentifier] autorelease];
}
cell.imageView.image = download.image;
return cell;
}
return nil;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
int t = [totalDownloads count];
return t;
}
ImageTableCell.m - Custom cell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self)
{
self.frame = CGRectMake(0.0f, 0.0f, 320.0f, 0.0f);
self.contentView.frame = CGRectMake(0.0f, 0.0f, 320.0f, 0.0f);
self.autoresizingMask = (UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth);
self.contentMode = UIViewContentModeScaleToFill;
self.autoresizesSubviews = YES;
self.contentView.autoresizingMask = (UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth);
self.contentView.contentMode = UIViewContentModeScaleToFill;
self.contentView.autoresizesSubviews = YES;
[self.imageView drawRect:CGRectMake(0.0f, 0.0f, 320.0f, 0.0f)];
self.imageView.contentMode = UIViewContentModeScaleAspectFill;
self.imageView.autoresizingMask = (UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth);
self.imageView.opaque = YES;
}
return self;
}
ImageDownloader (implements SDWebImageManagerDelegate)
-(void) downloadImage // Comes from Model class
{
if (image == nil)
{
NSURL *url = [NSURL URLWithString:self.urlString];
SDWebImageManager *manager = [SDWebImageManager sharedManager];
// Remove in progress downloader from queue
[manager cancelForDelegate:self];
if (url)
{
[manager downloadWithURL:url delegate:self retryFailed:YES];
}
}
}
- (void)cancelCurrentImageLoad
{
[[SDWebImageManager sharedManager] cancelForDelegate:self];
}
- (void)webImageManager:(SDWebImageManager *)imageManager didFinishWithImage:(UIImage *)_image
{
self.image = _image;
if ([self.delegate respondsToSelector:#selector(addImageToModel:)]) [self.delegate addImageToModel:self];
}
- (void)webImageManager:(SDWebImageManager *)imageManager didFailWithError:(NSError *)error;
{
if ([self.delegate respondsToSelector:#selector(badImage)]) [self.delegate badImage];
}
After you download the images, dont keep the large images in memory. just create a small size of image(thumbnail) to display in the tableview and write the larger image to some directory.
you can create a thumbnail of your image using the following code.
CGSize size = CGSizeMake(32, 32);
UIGraphicsBeginImageContext(size);
[yourImage drawInRect:CGRectMake(0, 0, 32, 32)];
yourImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
Instead of using [UIImage imageNamed:#""] , try [[[UIImage alloc] initWithContentsOfFile:#""] autorelease];
Edit:
Fine. I have gone through the SDWebImage.
Use NSAutoreleasePool wherever you find that a new thread has been spawned.
And one more solution would be, resize the image before saving to cache.
So basically once the image is downloaded it stays in it's SDWebImage instance and never gets released. You should save your image to iPhone's disk with:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
NSString *imagePath = [documentsPath stringByAppendingPathComponent:[NSString stringWithFormat:#"image%d.jpg", rowNumber]; // you should have this set somewhere
[UIImageJPEGRepresentation(_image, 1.0) writeToFile:imagePath atomically:YES];
and keep just the path to it in your SDWebImage instance. Then in -cellForRowAtIndexPath: method instead of doing:
cell.imageView.image = download.image;
you should do something like:
UIImage *image = [[UIImage alloc] initwithContentsOfFile:download.imagePath];
cell.imageView.image = image;
[image release];
This will always load the image from the disk and since cell.imageView.image is a retained property, once it get's niled or reused, it will clean up the image from memory.
I would like to know how you are loading the images, are you using custom cells? If so please go through the Apple's UITableView Programming guide They are clearly saying us how to load the images. In that they are saying we should need to draw the images top avoid the memory issues.
Best example on how to load images are given in Apple's Sample Code LazyTableImages Please go through this too.
I have used kingfisher SDK and resized the server image to my custom cell size. It helps me a lot.
extension NewsCollectionViewCell {
func configure(with news: Articles) {
KF.url(URL(string:news?.urlToImage ?? ""), cacheKey: "\(news?.urlToImage ?? "")-").downsampling(size: imgNews.frame.size).cacheOriginalImage().set(to: imgNews)
}
}