Loading images asynchronously in UITableView with Autolayout - ios

I have a UITableView which loads images asynchronously in a UITableView. As all the images are different heights, I set a height constraint according to the height of the image.
This works fine when I use DataWithContentsOfUrl, however freezes the UI. When I load asynchronously, the images pile up on top of each other like so:
http://i.stack.imgur.com/TEUwK.png
The code i'm using is as follows:
NSURL * imageURL = [NSURL URLWithString:img];
NSURLRequest* request = [NSURLRequest requestWithURL:imageURL];
cell.imageViewHeightConstraint.constant=0;
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * response, NSData * data, NSError * error) {
if (!error){
UIImage *imgz = [UIImage imageWithData:data];
[cell.CellImg setBackgroundImage:imgz forState:UIControlStateNormal];
[cell.CellImg sizeToFit];
cell.imageViewHeightConstraint.constant=result.width*(imgz.size.height/imgz.size.width);
[cell setNeedsUpdateConstraints];
}
}];
[cell updateConstraints];
When I scroll up and down a few times the images correctly position themselves.

The question doesn't specify the desired behavior for varying image sizes. Should the cell get taller to fit, or just the image view (what looks like a button from the code) within the cell?
But we should put aside that problem for a moment and work on the more serious problem with the code: it issues an unguarded network request from cellForRowAtIndexPath. As a result, (a) a user scrolling back and forth will generate many many redundant requests, and (b) a user scrolling a long way quickly will generate a request that's fulfilled when the cell that started it is gone - reused as the cell for another row.
To address (a), the datasource should cache fetched images, and only request those that haven't been received. To address (b), the completion block shouldn't refer directly to the cell.
A simple cache would look like this:
#property(strong,nonatomic) NSMutableDictionary *images;
// initialize this when you initialize your model
self.images = [#{} mutableCopy];
// move the network code into its own method for clarity
- (void)imageWithPath:(NSString *)path completion:(void (^)(UIImage *, NSError *))completion {
if (self.images[indexPath]) {
return completion(self.images[indexPath], nil);
}
NSURL *imageURL = [NSURL URLWithString:path];
NSURLRequest *request = [NSURLRequest requestWithURL:imageURL];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (!error){
UIImage *image = [UIImage imageWithData:data];
self.images[indexPath] = image;
completion(image, nil);
} else {
completion(nil, error);
}
}];
}
Now, we fix the multiple request problem by checking first for the image in the cache in cellForRowAtIndexPath.
UIImage *image = self.images[indexPath];
if (image) {
[cell.CellImg setBackgroundImage:image forState:UIControlStateNormal];
} else {
// this is a good place for a placeholder image if you want one
[cell.CellImg setBackgroundImage:nil forState:UIControlStateNormal];
// presuming that 'img' is a string from your mode
[self imageWithPath:img completion:^(UIImage *image, NSError *error) {
// the image is ready, but don't assign it to the cell's subview
// just reload here, so we get the right cell for the indexPath
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}];
}
Also notice what isn't done in the completion block... we're fixing the cell reuse by not referring to the cell. Instead, knowing that the image is now cached, we reload the indexPath.
Back to image sizing: most apps like to see the table view cell get taller or shorter along with the variable height subview. If that's the case, then you should not place a height constraint on that subview at all. Instead, constrain it's top and bottom edges to the cell's content view (or include it in a chain of subviews who constrain to each other top and bottom and contain the topmost and bottommost subviews top and bottom edge to the cell). Then (in iOS 5+, I think), this will allow your cell to change hight with that subview constraint chain...
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = // your best guess at the average height here

Try the following along with constraint constant:
cell.imageViewHeightConstraint.constant=result.width*(imgz.size.height/imgz.size.width);
[table reloadRowsAtIndexPaths:#[indexPathForCurrentCell] withRowAnimation:UITableViewRowAnimationNone];
You can also set the animation.

Although reloadRowsAtIndexPaths works, it ends up causing the interface to be slow and laggy, especially when the user is scrolling. My solution was once the data was loaded to iterate through https://graph.facebook.com/v2.1/%#/?access_token=%#&fields=attachments for the object id, and then store the image height dimensions in an array. Then, in cellForRowAtIndexPath simply setting cell.imageViewHeightConstraint.constant to the value in the array. That way all the cells heights are set as soon as the cell loads, and the image fits into place perfectly as soon as it loads.

Related

How would I get my image to occupy the whole image view?

I have an image view with constraints set up, and the image that I load in (from a URL) doesn't fill the whole image view for some reason. I included a picture of the one in question followed by the detail view of the same 'post'. The same code is used for both. In the detail view (one that appears correctly), I set the post image in during viewdidload, and it's a standalone view. The one that loads in wonky is a table view cell with a custom class, and the image is set in the cellForRowAtIndexPath. I do resize the image before I send it to Back4App with parse to 500x500, and the image views are both set to clip to bounds and aspect fit. the uiimageview constraints are exactly the same, with horizontal alignment of zero, a fixed width and height, and constraints for all four sides (8 away from nearest neighbors)
here are code snippets for how both of them are done:
pink background:
- (nonnull UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
PostCell *cell = [self.tableView dequeueReusableCellWithIdentifier:#"PostCell"];
Post *post = self.posts[indexPath.row];
PFUser *user = post.author;
[user fetchInBackgroundWithBlock:^(PFObject * _Nullable object, NSError * _Nullable error) {
cell.usernameLabel.text = post.author.username;
}];
cell.titleLabel.text = post.caption;
cell.isLiked = NO;
NSURL *imageURL = [NSURL URLWithString:post.image.url];
[cell.imageView setImageWithURL:imageURL];
return cell;
}
one that loads correctly:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
PFUser *user = self.post.author;
[user fetchInBackgroundWithBlock:^(PFObject * _Nullable object, NSError * _Nullable error) {
self.usernameTextLabel.text = self.post.author.username;
}];
self.titleTextLabel.text = self.post.caption;
self.descriptionTextLabel.text = self.post.bodyText;
NSURL *postImageURL = [NSURL URLWithString:self.post.image.url];
[self.postImageView setImageWithURL:postImageURL];'''
}
Any tips? There may be some basics that I don't have a handle on - i took a crash course basically and I've only been at this for about 3 weeks so I don't understand a lot of this as deeply as I wish I did.
I've set the background color to pink so you could see the image size vs image view
one that loads correctly
side note: i literally can't figure out how to do blocks of code correctly??? i'm following what it says lol
Check following points
Debug the frame size of the ImageView that it is correct which you supplied.
Set following to the ImageView
[cell.imageView setImageWithURL:imageURL];
cell.imageView.contentMode = UIViewContentModeScaleAspectFill;
[cell.imageView setClipsToBounds:YES];
I was accidentally using the default imageView that the cell comes with instead of my own outlet to my new image view.

How to auto-layout the UIImageview base on its image size after downloading the image via SDWebImage

I built a sample project hosted on GitHub.
There is a table view and its custom cell has a multiple line UILabel and a dynamic image view base on its image size. I built it step by step to verify the auto-layout issue on the custom cell.
In the 3rd sample table view (Table3ViewController) which has the above view structure, I tested it with sample random length text and local images. It succeeds, just like Apple documentation and this thread said.
Actually, all the auto-layout tutorials or examples from google search results are tested base on the local image assets, no SDWebImage usage related, basically.
In Table4ViewController, the UIImageView/UITableViewCell(CustomCell) finished its subview's layout before the SDWebImage set the final image object to UIImageView asynchronously.
Here is my question, how to re-layout the UITableViewCell(CustomCell) after the SDWebImage downloaded the image from the network?
Here are my constraints within CustomCell.
[label mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.contentView.top);
make.centerX.width.equalTo(self.contentView);
make.bottom.equalTo(imageView.top);
make.height.greaterThanOrEqualTo(#16);
}];
[imageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(label.bottom);
make.bottom.equalTo(self.contentView.bottom);
make.centerX.width.equalTo(self.contentView);
make.height.lessThanOrEqualTo(#200);
}];
I tried to move the implementation to initWithStyle:reuseIdentifier: or updateConstraints or layoutSubviews method, all the results are the same and failed.
Here is the image view assignment within cellForRow without placeholder image.
__weak __typeof(cell) weakCell = cell;
[cell.theImageView sd_setImageWithURL:[NSURL URLWithString:dict.allValues.firstObject]
completed:^(UIImage *_Nullable image, NSError *_Nullable error, SDImageCacheType cacheType, NSURL *_Nullable imageURL) {
__strong __typeof(cell) strongCell = weakCell;
[strongCell setNeedsUpdateConstraints];
[strongCell updateConstraintsIfNeeded];
}];
Assume a network image has size 400x400 points, there is no reserved placeholder image in cell, the update constraint and layout subviews' request will be ready and finished asynchronously successfully. However, the new cell's height won't be recalculated into the contentSize of tableview, the draw process of CustomCell won't be called. It fails.
Here is the image view assignment within cellForRow with placeholder image.
__weak __typeof(cell) weakCell = cell;
[cell.theImageView sd_setImageWithURL:[NSURL URLWithString:dict.allValues.firstObject]
placeholderImage:GetImageWithColor(DarkRandomColor, CGRectGetWidth(tableView.frame), 400)
completed:^(UIImage *_Nullable image, NSError *_Nullable error, SDImageCacheType cacheType, NSURL *_Nullable imageURL) {
__strong __typeof(cell) strongCell = weakCell;
[strongCell setNeedsUpdateConstraints];
[strongCell updateConstraintsIfNeeded];
}];
The final image will be drawn with the same size of reserved placeholder image finally, but I expected the final image's size even specified ratio.
So, how to re-layout the image view's size constraint after downloading the image asynchronously?

iOS CollectionView: Display View, then download images, then display images

I have a collection view, which contains cells in each section, and each cell has an image in it.
Possibly, the collection view holds 20 or more cells.
I want to load the images off the internet into the cells. The issue is that this can take a few seconds, and I want the collectionView to be displayed even if the images have not downloaded completely.
The collection view is given an array that contains the URLs, and so far, I have been downloading off the internet within collection view cellforitematindexpath
However, the view only becomes visible after all the cells have been loaded, and since each call to collection view cellforitematindexpath downloads an image, if 20 images are pulled off of URLs, it takes way to long.
What can I do to display the view, and THEN download the images, and then display them?
Hope I made myself understandable, if not, please ask!
Thanks!
C
Yes, you could use SDWebImage (as mention at comment above).
Another interesting side you could face out, that if image haven't load yet and user scroll view, it could be problems with dequeueReusableCellWithReuseIdentifier:forIndexPath, so it's better to download images throw id <SDWebImageOperation>.
Prepare for reuse will be something like this:
- (void)prepareForReuse
{
[super prepareForReuse];
if (self.imageOperation)
[self.imageOperation cancel];
self.imageOperation = nil;
}
And load will be like this:
SDWebImageManager *manager = [SDWebImageManager sharedManager];
self.imageOperation = [manager downloadWithURL:[NSURL URLWithString:urlString]
options:SDWebImageRetryFailed
progress:nil
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished) {
if (image)
{
self.imageView.image = image;
}
}];

UIImageView+AFNetworking Success Block with resize image and return image height to resize cell height

I am using UIImageView+AFNetworking to get the image URL and using that Image to resize if the size is big. and if the size is big I need to return the height of the image in the
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
The problem is the height is returned before the block is executed and resize is returned. I want to wait till the the process which I am carrying out in the success block. to complete and then return the image size in the method.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
UIImageView *ivImage = [[UIImageView alloc]init];
__block BOOL isResized = NO;
[ivImage setImageWithURLRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[[marrMainNewsFeed objectAtIndex:indexPath.row] valueForKey:#"image"]]] placeholderImage:[UIImage imageNamed:#"placeholder.png"] success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
// cell.ivImage.image = image;
NSLog(#"image size %f",image.size.width);
if (ivImage.image.size.width >= SCREEN_WIDTH - 10) {
// I resize my image by calling following method
ivImage.image = [CommonFunctions imageWithImage:ivImage.image scaledToMaxWidth:SCREEN_WIDTH maxHeight:0];
ivImage.frame = CGRectMake(ivImage.frame.origin.x, ivImage.frame.origin.y, SCREEN_WIDTH - 4, ivImage.image.size.height);
isResized = YES;
ivImage.image = image;
// height = ivImage.image.size.height;
} else {
ivImage.image = image;
ivImage.contentMode = UIViewContentModeCenter;
// height = ivImage.image.size.height;
}
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
NSLog(#"Request failed with error: %#", error);
}];
NSLog(#"new Height %# for index %#",isResized?#"YES":#"NO",indexPath);
return height + 350;
}
The return is fired first and then the image is resized and gives the height of the image. I want to wait till the resize is completed and then return the height of the cell.
Hope I would get the fruitful resutls
This is absolutely the wrong thing to do.
A URL request could take a very long time - if you did actually wait until it the image had been fetched and resized, then you could be blocking your UI for so long that you'd run the risk of the user assuming your app had crashed, and force quitting it. You'd also run an excellent chance of failing Apple testing.
If you need to know how big your image is, for layout purposes, then the only options you have are:
1) Do all of the fetching before you ever display this view
or
2) Depending on your application and server architecture, prefetch information about the layout before you fetch all the images.
The alternative and simpler course of action is to relayout once the image has been fetched.

UITableView with images crashes app when I scroll down a lot

I have this simple UITableView and each cell has an image corresponding to it. All I'm doing is displaying a title for the image and the image itself in each cell. Here is my cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// this is where the data for each cell is
NSDictionary *dataForThisCell = cachedData.posts[indexPath.row][#"data"];
// this creates a new cell or grabs a recycled one, I put NSLogs in the if statement to make sure they are being recycled, they are.
post *cell = (post *) [self.tableView dequeueReusableCellWithIdentifier:#"postWithImage"];
if (cell == nil) {
cell = [[[NSBundle mainBundle]loadNibNamed:#"postWithImage" owner:self options:nil]objectAtIndex:0];
[cell styleCell];
}
// if this cell has an image we need to stick it in the cell
NSString *lowerCaseURL = [dataForThisCell[#"url"] lowercaseString];
if([lowerCaseURL hasSuffix: #"gif"] || [lowerCaseURL hasSuffix: #"bmp"] || [lowerCaseURL hasSuffix: #"jpg"] || [lowerCaseURL hasSuffix: #"png"] || [lowerCaseURL hasSuffix: #"jpeg"]) {
// if this cell doesnt have an UIImageView, add one to it. Cells are recycled so this only runs several times
if(cell.preview == nil) {
cell.preview = [[UIImageView alloc] init];
[cell.contentView addSubview: cell.preview];
}
// self.images is an NSMutableDictionary that stores the width and height of images corresponding to cells.
// if we dont know the width and height for this cell's image yet then we need to know now to store it
// once the image downloads, and then cause our table to reload so that heightForRowAtIndexPath
// resizes this cell correctly
Boolean shouldReloadData = self.images[dataForThisCell[#"name"]] == nil ? YES : NO;
// download image
[cell.preview cancelImageRequestOperation];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString: dataForThisCell[#"url"]]];
[request addValue:#"image/*" forHTTPHeaderField:#"Accept"];
[cell.preview setImageWithURLRequest: request
placeholderImage: [UIImage imageNamed:#"thumbnailLoading.png"]
success: ^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
// if we indicated earlier that we didnt know the dimensions of this image until
// just now after its been downloaded, then store the image dimensions in self.images
// and tell the table to reload so that heightForRowAtIndexPath
// resizes this cell correctly
if(shouldReloadData) {
NSInteger imageWidth = image.size.width;
NSInteger imageHeight = image.size.height;
if(imageWidth > [ColumnController columnWidth]) {
float ratio = [ColumnController columnWidth] / imageWidth;
imageWidth = ratio * imageWidth;
imageHeight = ratio* imageHeight;
}
if(imageHeight > 1024) {
float ratio = 1024 / imageHeight;
imageHeight = ratio * imageHeight;
imageWidth = ratio* imageWidth;
}
self.images[dataForThisCell[#"name"]] = #{ #"width": #(imageWidth), #"height": #(imageHeight), #"titleHeight": #([post heightOfGivenText: dataForThisCell[#"title"]]) };
[self.tableView reloadData];
// otherwise we alreaady knew the dimensions of this image so we can assume
// that heightForRowAtIndexPath has already calculated the correct height
// for this cell
}else{
// assign the image we downloaded to the UIImageView within the cell
cell.preview.image = image;
// position the image
NSInteger width = [self.images[dataForThisCell[#"name"]][#"width"] integerValue];
NSInteger height = [self.images[dataForThisCell[#"name"]][#"height"] integerValue];
cell.preview.frame = CGRectMake( ([ColumnController columnWidth] - width)/2 , [self.images[dataForThisCell[#"name"]][#"titleHeight"] integerValue] + 10, width, height);
}
}
failure: ^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {}];
}
// set title of the cell
cell.title.text = [NSString stringWithFormat: #"%#\n\n\n\n\n", dataForThisCell[#"title"]];
`enter code here`// ask for a restyle
[cell setNeedsLayout];
// returns my customized cell
return cell;
}
What happens is that everything works exactly how I want it to, however once I scroll down past around 100 cells or so the background of my app goes black for a few seconds and then I see my homescreen (I've seen some people call this the HSOD - home screen of death). Sometimes in the console in xcode I see memory warnings before a crash and sometimes I do not.
I know for a fact that whatever the problem is, it has to do with putting images into the cells. If I comment out just this line:
cell.preview.image = image;
Then everything works fine and it doesn't crash any more (but then of course the images are not being displayed in the cells).
The cells are being reused and I know that's working, for good measure I set the UIImageView's image property to nil:
- (void) prepareForReuse {
[super prepareForReuse];
if(self.preview != nil)
self.preview.image = nil;
}
and in my appDelegate I also define this:
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
[UIImageView clearAFImageCache];
}
Which deletes the image cache but that doesn't fix the problem either (and anyway iOS should clear the image caches upon memory warnings automatically anyway).
I ran analyze on my project and it reports no memory leaks, and here is the profiler showing that, as well as showing the allocations at the time of the crash:
Other than the occasional memory warning in the console which appears about 2/3rds of the time the app crashes, there are no other errors that appear in the console, and I do not hit any breakpoints or exceptions.
All of those allocations are you creating new table view cells each time they're requested, rather than reusing existing ones. Without setting a reuseIdentifier for cells created from UINib, dequeueReusableCellWithIdentifier: will always return `nil.
To fix this, add the following code (as referenced in this question):
[self.tableView registerNib:[UINib nibWithNibName:#"nibname"
bundle:nil]
forCellReuseIdentifier:#"cellIdentifier"];
I've found a solution, albeit not a perfect one:
The AFNetworking library is brilliant and I assume the cause of my problem lies within my own code or my lack of understanding as to how NSCache works.
AFNetworking caches images using Apple's NSCache. NSCache is similar to NSMutableDictionary, but releases objects when memory is spread thin (see more here).
Within UIImageView+AFNetworking.m I located the definition of
+ (AFImageCache *)af_sharedImageCache
And altered it to resemble this:
+ (AFImageCache *)af_sharedImageCache {
static AFImageCache *_af_imageCache = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_af_imageCache = [[AFImageCache alloc] init];
_af_imageCache.countLimit = 35;
});
return _af_imageCache;
}
The important line here is
_af_imageCache.countLimit = 35;
This tells the NSCache object being used in AFNetworking to cache images that it must only cache up to a maximum of 35 things.
For reasons unknown to me, iOS was not automatically purging objects from the cache as it should, and calling removeAllObjects on the cache was not working either. This solution is hardly ideal because it may not utilize the cache to its full potential or may over use the cache, but for the meantime it atleast stops the cache from attempting to store an infinite number of objects.

Resources