UITableView async image loading - not laying out until scroll - ios

My asynchronously loading images aren't loading in my initial cells when the tableView first loads. They only show after I scroll the cells off-screen and them scroll them on-screen again. So I've read a number of S.O. questions on this issue, and I feel like I'm doing everything right. There's a lot going on in this method as it's populating a UIView with a number of avatar images.
I'm retrieving the images in a background thread, and then when I'm setting the images I go back to the main thread. And then I'm calling this:
[correctCell setNeedsLayout];
It appears to me that I'm doing everything correct but it seems like setNeedsLayout just isn't getting called back on the main thread.
Here's the method which I'm calling from tableView:cellForRowAtIndexPath::
- (void) setPeopleAvatars:(NSArray*)people cell:(WSExploreTableViewCell*)cell indexPath:(NSIndexPath *) indexPath{
[cell clearPeopleAvatars];
// Dimensions
CGFloat maxAvatars = cell.peopleView.frame.size.width / (kWSExploreCellPeopleAvatarDim + kWSExploreCellPeopleAvatarPadding);
CGFloat peopleAvatarX = cell.peopleView.frame.size.width - kWSExploreCellConvoBubblePadding - kWSExploreCellPeopleAvatarDim;
CGFloat peopleAvatarY = cell.peopleView.frame.size.height/2 - kWSExploreCellPeopleAvatarDim/2;
__block CGFloat lastAvatarX;
__block NSUInteger numAvatars = 0;
// First set the convo bubble image
cell.convoImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:kWSExploreCellConvoBubbleIcon]];
cell.convoImageView.frame = CGRectMake(cell.peopleView.frame.size.width - 1.0f - cell.convoImageView.frame.size.width,
cell.peopleView.frame.size.height/2 - cell.convoImageView.frame.size.height/2,
cell.convoImageView.frame.size.width,
cell.convoImageView.frame.size.height);
[cell.peopleView addSubview:cell.convoImageView];
cell.convoImageView.hidden = YES;
if (people.count == 0) {
cell.convoImageView.hidden = NO;
}
[people enumerateObjectsUsingBlock:^(id person, NSUInteger idx, BOOL *stop) {
// Stop at maxAvatars
if (idx >= maxAvatars) {
*stop = YES;
return;
}
[WSDatabaseManager getSmallProfilePic:(PFUser*)person callback:^(UIImage *image, NSError *error) {
if (!error) {
dispatch_async(dispatch_get_main_queue(), ^{
WSExploreTableViewCell * correctCell = (WSExploreTableViewCell*)[self.tableView cellForRowAtIndexPath:indexPath];
WSProfileImageView* avatarImageView = [[WSProfileImageView alloc] initWithImage:image croppedToCircle:YES diameter:kWSExploreCellPosterAvatarDim];
[avatarImageView setBackgroundColor:[UIColor clearColor]];
[avatarImageView setOpaque:NO];
[correctCell.peopleView addSubview:avatarImageView];
// Layout
CGFloat totalAvatarWidth = kWSExploreCellPeopleAvatarDim + kWSExploreCellPeopleAvatarPadding;
lastAvatarX = peopleAvatarX - (numAvatars * totalAvatarWidth);
[avatarImageView setFrame:CGRectMake(lastAvatarX, peopleAvatarY, kWSExploreCellPeopleAvatarDim, kWSExploreCellPeopleAvatarDim)];
// Move the convoImageView
CGRect convoFrame = correctCell.convoImageView.frame;
convoFrame.origin.x = convoFrame.origin.x - totalAvatarWidth;
correctCell.convoImageView.frame = convoFrame;
correctCell.convoImageView.hidden = NO;
// Update Counter
numAvatars++;
[correctCell setNeedsLayout];
});
}
}];
}];
}

As you probably know, the cell gets redrawn when it first appears on screen. That's why you are seeing the cell update when they reappear. You can try force refreshing the cell once you are done updating it.
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;

Try SDWebImage
It will take care of loading your images when they are retrieved and will cache also. All you have to do is import UIImageView+WebCache.h :
[convoImageView setImageWithURL:YOUR_IMAGE_URL_HERE
placeholderImage:[UIImage imageNamed:#"placeholder.png"]
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType) {
//do something in completion block
}];

Related

Resize UITableViewCell to fit label or image and rounded corners

I am attaching a sample project to allow you to test and see what I can do to fix this.
I am trying to resize the height's image dynamically and at the same time round two corners on every other cell. Doing so results in a mask that is cut off.
Basically here is what I am trying to achieve:
UITableView's row height is set to automatic.
_nTableView.rowHeight = UITableViewAutomaticDimension;
_nTableView.estimatedRowHeight = 150;
Download images using SDWebImage library using a CustomTableViewCell
[self.nImageView sd_setImageWithURL:[NSURL URLWithString:imageUrl] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL){}];
Configure a cell to round the corners depending on its row
UIBezierPath *maskPath = [UIBezierPath
bezierPathWithRoundedRect:self.bubbleView.bounds
byRoundingCorners:(UIRectCornerBottomLeft | UIRectCornerTopLeft)
cornerRadii:CGSizeMake(20, 20)];
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = self.bubbleView.bounds;
maskLayer.path = maskPath.CGPath;
self.bubbleView.layer.mask = maskLayer;
Doing the above results in a mask being cut off but the height is calculated correctly. See below:
Based on this stack overflow's question, we need to remove the mask if there is one. I have tried to set the mask to nil in awakeFromNib method but that has no affect.
self.nImageView.layer.mask = nil;
self.nImageView.layer.masksToBounds = NO;
I also tried following the answers from this question but it results in the app crashing.
Thank you in advance.
Here is one possible solution - may not be the best, may not do exactly what you need, but might get you on your way...
Instead of trying to manipulate the layers / masks from within your custom UITableViewCell class, use a UIView subclass as the image holder. The code of that class can handle the rounded corners and layer/mask size updating.
See example here: https://github.com/DonMag/dhImage
To set height of row based on it's content:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(/*check if content is text label*/)
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell1"];
UILabel *lbl = (UILabel*)[ cell.contentView viewWithTag:1000];
lbl.text = message;
[cell layoutIfNeeded];
CGSize size = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
if(size.height < 100) {
return 100;
}
else {
return size.height;
}
}
else { // content is image
return 200;
}
}
Configure a cell to round the corners:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
.
.
.
[cell.contentView.layer setCornerRadius:10.0f];
return cell;
}
To Download images using SDWebImage library:
SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
[downloader downloadImageWithURL:addr
options:0
progress:
^(NSInteger receivedSize, NSInteger expectedSize) {
// progression tracking code
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
if (image && finished) {
// Pass the imageView to this block
// After completion display downloaded image in imageView
imageView.image = image;
}
}];
Hope it helps!!

Altenating images in image view using thread

I have the following code to toggle between two images in a image view. I am not using GCD because this code is part of an already existing system that has been coded this way.
- (void)updateimage {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.fileno % 2 == 0) {
self.imageViewTest.image = [UIImage imageNamed:#"Image7.png"];
}
else {
self.imageViewTest.image = [UIImage imageNamed:#"Image8.png"];
}
[self.imageViewTest setNeedsDisplay];
self.filenolabel.text = [NSString stringWithFormat:#"%d", self.fileno];
});
}
- (void)calculateFileNo {
while (1) {
self.fileno ++;
sleep(1);
[self updateimage];
}
}
- (void)viewDidLoad {
[super viewDidLoad];
self.detectionThread = [[NSThread alloc]initWithTarget:self selector:#selector(calculateFileNo) object:nil] ;
[self.detectionThread setName:#"NewThread"];
[self.detectionThread start];
}
The filenolabel label displays the right no for each loop. But at times the images do not toggle properly. The same image is displayed for 2 or 3 iterations of the loop. I want the image to be toggled for each iteration. Please help. Thanks in advance
Increment place is a problem 'self.fileno ++;' -> move it to dispatcher.
- (void)updateimage {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.fileno % 2 == 0) {
self.imageViewTest.image = [UIImage imageNamed:#"Image7.png"];
}
else {
self.imageViewTest.image = [UIImage imageNamed:#"Image8.png"];
}
[self.imageViewTest setNeedsDisplay];
self.filenolabel.text = [NSString stringWithFormat:#"%d", self.fileno];
self.fileno ++;
});
}
- (void)calculateFileNo {
while (1) {
sleep(1);
[self updateimage];
}
}
Increase your file no inside the main thread.
self.fileno++;
You should only be calling setNeedsDisplay if you override drawRect in a subclass of UImageView which is basically a custom view drawing something on the screen, like lines, images, or shapes like a rectangle.
So you should call setNeedsDisplay when you make changes to few variables on which this drawing depends and for view to represent that change , you need to call this method which internally will give a call to drawRect and redraw the components.
When you change an imageView of image or make changes to any subview, you need call this method.
- (void)updateimage {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.fileno % 2 == 0) {
self.imageView.image = [UIImage imageNamed:#"Image7.png"];
}
else {
self.imageView.image = [UIImage imageNamed:#"Image8.png"];
}
[self.imageView setNeedsDisplay];
self.fileno ++; // Increament your file no inside the main thread.
self.textView.text = [NSString stringWithFormat:#"%d", self.fileno];
});
}

UITableViewCell ImageView Resize - Selected Row Changes Image Size

I am setting an table cell ImageView from a URL like the following:
NSURL *url = [[NSURL alloc] initWithString:[account valueForKeyPath:#"Avatar"]];
[cell.imageView sd_setImageWithURL:url placeholderImage:[UIImage imageNamed:#"default.png"]];
I am then using a table cell subclass and resizing the image and making it round like the following:
- (void)layoutSubviews {
[super layoutSubviews];
self.imageView.bounds = CGRectMake(0,0,32,32);
self.imageView.layer.cornerRadius = 16;
self.imageView.clipsToBounds = YES;
}
This works great until I actually tap on a table row. It will then shift the image to the left some pixels.
It's only the images that aren't a perfect square. Example 100x75 instead of 100x100
I must be missing something small here? Here is a small image showing the offset:
FYI I'm referencing this post: How do I make UITableViewCell's ImageView a fixed size even when the image is smaller
EDIT
I've also tried this as the table cell cellForRowAtIndexPath:
This seems to work, however, for some reason when the table view is loaded not all images are loaded in until I start scrolling
[[SDWebImageManager sharedManager] downloadImageWithURL:url options:0 progress: nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
// resize image code here, then set below
cell.imageView.image = image;
}];
In my case I did this
-(void)layoutSubviews
{
[super layoutSubviews];
[self layoutIfNeeded];
self.imageView.layer.cornerRadius = (self.imageView.bounds.size.width / 2.0);
self.imageView.layer.masksToBounds = YES;
}
I haven't change bounds at run time. Hope it helps
FYI I ended up just creating a custom UITableViewCell with a UIImageView defined width/height 32x32 and custom labels to essentially mimic the Right Detail view.
You can try adding cell.setNeedsLayout() after setting Cell's image view, if all images have same dimensions!
MenuController.shared.fetchImage(url: item.imageURL) { (image) in
if let image = image {
DispatchQueue.main.async {
cell.imageView?.image = image
cell.setNeedsLayout()
}
}
}
Try this:
- (void)layoutSubviews {
[super layoutSubviews];
self.imageView.frame = CGRectMake(0,self.frame.size.width/2-16,32,32);
self.imageView.layer.cornerRadius = 16;
self.imageView.clipsToBounds = YES;
}
Or you may try set autoresizingMask

UICollection View Scroll lag with SDWebImage

Background
I have searched around SO and apple forum. Quite a lot of people talked about performance of collection view cell with image. Most of them said it is lag on scroll since loading the image in the main thread.
By using SDWebImage, the images should be loading in separate thread. However, it is lag only in the landscape mode in the iPad simulator.
Problem description
In the portrait mode, the collection view load 3 cells for each row. And it has no lag or insignificant delay.
In the landscape mode, the collection view load 4 cells for each row. And it has obvious lag and drop in frame rate.
I have checked with instrument tools with the core animation. The frame rate drop to about 8fps when new cell appear. I am not sure which act bring me such a low performance for the collection view.
Hope there would be someone know the tricks part.
Here are the relate code
In The View Controller
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
ProductCollectionViewCell *cell=[collectionView dequeueReusableCellWithReuseIdentifier:#"ProductViewCell" forIndexPath:indexPath];
Product *tmpProduct = (Product*)_ploader.loadedProduct[indexPath.row];
cell.product = tmpProduct;
if (cellShouldAnimate) {
cell.alpha = 0.0;
[UIView animateWithDuration:0.2
delay:0
options:(UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction)
animations:^{
cell.alpha = 1.0;
} completion:nil];
}
if(indexPath.row >= _ploader.loadedProduct.count - ceil((LIMIT_COUNT * 0.3)))
{
[_ploader loadProductsWithCompleteBlock:^(NSError *error){
if (nil == error) {
cellShouldAnimate = NO;
[_collectionView reloadData];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
cellShouldAnimate = YES;
});
} else if (error.code != 1){
#ifdef DEBUG_MODE
ULog(#"Error.des : %#", error.description);
#else
CustomAlertView *alertView = [[CustomAlertView alloc]
initWithTitle:#"Connection Error"
message:#"Please retry."
buttonTitles:#[#"OK"]];
[alertView show];
#endif
}
}];
}
return cell;
}
PrepareForReuse in the collectionViewCell
- (void)prepareForReuse
{
[super prepareForReuse];
CGRect bounds = self.bounds;
[_thumbnailImgView sd_cancelCurrentImageLoad];
CGFloat labelsTotalHeight = bounds.size.height - _thumbnailImgView.frame.size.height;
CGFloat brandToImageOffset = 2.0;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
brandToImageOffset = 53.0;
}
CGFloat labelStartY = _thumbnailImgView.frame.size.height + _thumbnailImgView.frame.origin.y + brandToImageOffset;
CGFloat nameLblHeight = labelsTotalHeight * 0.46;
CGFloat priceLblHeight = labelsTotalHeight * 0.18;
_brandLbl.frame = (CGRect){{15, labelStartY}, {bounds.size.width - 30, nameLblHeight}};
CGFloat priceToNameOffset = 8.0;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
priceToNameOffset = 18.0;
}
_priceLbl.frame = (CGRect){{5, labelStartY + nameLblHeight - priceToNameOffset}, {bounds.size.width-10, priceLblHeight}};
[_spinner stopAnimating];
[_spinner removeFromSuperview];
_spinner = nil;
}
Override the setProduct method
- (void)setProduct:(Product *)product
{
_product = product;
_spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
_spinner.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
[self addSubview:_spinner];
[_spinner startAnimating];
_spinner.hidesWhenStopped = YES;
// Add a spinner
__block UIActivityIndicatorView *tmpSpinner = _spinner;
__block UIImageView *tmpImgView = _thumbnailImgView;
ProductImage *thumbnailImage = _product.images[0];
[_thumbnailImgView sd_setImageWithURL:[NSURL URLWithString:thumbnailImage.mediumURL]
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
// dismiss the spinner
[tmpSpinner stopAnimating];
[tmpSpinner removeFromSuperview];
tmpSpinner = nil;
if (nil == error) {
// Resize the incoming images
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGFloat imageHeight = image.size.height;
CGFloat imageWidth = image.size.width;
CGSize newSize = tmpImgView.bounds.size;
CGFloat scaleFactor = newSize.width / imageWidth;
newSize.height = imageHeight * scaleFactor;
UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0);
[image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
UIImage *small = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(),^{
tmpImgView.image = small;
});
});
if (cacheType == SDImageCacheTypeNone) {
tmpImgView.alpha = 0.0;
[UIView animateWithDuration:0.2
delay:0
options:(UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction)
animations:^{
tmpImgView.alpha = 1.0;
} completion:nil];
}
} else {
// loading error
[tmpImgView setImage:[UIImage imageNamed:#"broken_image_small"]];
}
}];
_brandLbl.text = [_product.brand.name uppercaseString];
_nameLbl.text = _product.name;
[_nameLbl sizeToFit];
// Format the price
NSNumberFormatter * floatFormatter = [[NSNumberFormatter alloc] init];
[floatFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
[floatFormatter setDecimalSeparator:#"."];
[floatFormatter setMaximumFractionDigits:2];
[floatFormatter setMinimumFractionDigits:0];
[floatFormatter setGroupingSeparator:#","];
_priceLbl.text = [NSString stringWithFormat:#"$%# USD", [floatFormatter stringFromNumber:_product.price]];
if (_product.salePrice.intValue > 0) {
NSString *rawStr = [NSString stringWithFormat:#"$%# $%# USD", [floatFormatter stringFromNumber:_product.price], [floatFormatter stringFromNumber:_product.salePrice]];
NSMutableAttributedString * string = [[NSMutableAttributedString alloc] initWithString:rawStr];
// Change all the text to red first
[string addAttribute:NSForegroundColorAttributeName
value:[UIColor colorWithRed:157/255.0 green:38/255.0 blue:29/255.0 alpha:1.0]
range:NSMakeRange(0,rawStr.length)];
// find the first space
NSRange firstSpace = [rawStr rangeOfString:#" "];
// Change from zero to space to gray color
[string addAttribute:NSForegroundColorAttributeName
value:_priceLbl.textColor
range:NSMakeRange(0, firstSpace.location)];
[string addAttribute:NSStrikethroughStyleAttributeName
value:#2
range:NSMakeRange(0, firstSpace.location)];
_priceLbl.attributedText = string;
}
}
SDWebImage is very admirable, but DLImageLoader is absolutely incredible, and a key piece of many big production apps
https://stackoverflow.com/a/19115912/294884
it's amazingly easy to use.
To avoid the skimming problem, basically just introduce a delay before bothering to start downloading the image. So, essentially like this...it's this simple
dispatch_after_secs_on_main(0.4, ^
{
if ( ! [urlWasThen isEqualToString:self.currentImage] )
{
// so in other words, in fact, after a short period of time,
// the user has indeed scrolled away from that item.
// (ie, the user is skimming)
// this item is now some "new" item so of course we don't
// bother loading "that old" item
// ie, we now know the user was simply skimming over that item.
// (just TBC in the preliminary clause above,
// since the image is already in cache,
// we'd just instantly load the image - even if the user is skimming)
// NSLog(#" --- --- --- --- --- --- too quick!");
return;
}
// a short time has passed, and indeed this cell is still "that" item
// the user is NOT skimming, SO we start loading the image.
//NSLog(#" --- not too quick ");
[DLImageLoader loadImageFromURL:urlWasThen
completed:^(NSError *error, NSData *imgData)
{
if (self == nil) return;
// some time has passed while the image was loading from the internet...
if ( ! [urlWasThen isEqualToString:self.currentImage] )
{
// note that this is the "normal" situation where the user has
// moved on from the image, so no need toload.
//
// in other words: in this case, not due to skimming,
// but because SO much time has passed,
// the user has moved on to some other part of the table.
// we pointlessly loaded the image from the internet! doh!
//NSLog(#" === === 'too late!' image load!");
return;
}
UIImage *image = [UIImage imageWithData:imgData];
self.someImage.image = image;
}];
});
That's the "incredibly easy" solution.
IMO, after vast experimentation, it actually works considerably better than the more complex solution of tracking when the scroll is skimming.
once again, DLImageLoader makes all this extremely easy https://stackoverflow.com/a/19115912/294884
Note that the section of code above is just the "usual" way you load an image inside a cell.
Here's typical code that would do that:
-(void)imageIsNow:(NSString *)imUrl
{
// call this routine o "set the image" on this cell.
// note that "image is now" is a better name than "set the image"
// Don't forget that cells very rapidly change contents, due to
// the cell reuse paradigm on iOS.
// this cell is being told that, the image to be displayed is now this image
// being aware of scrolling/skimming issues, cache issues, etc,
// utilise this information to apprporiately load/whatever the image.
self.someImage.image = nil; // that's UIImageView
self.currentImage = imUrl; // you need that string property
[self loadImageInASecIfItsTheSameAs:imUrl];
}
-(void)loadImageInASecIfItsTheSameAs:(NSString *)urlWasThen
{
// (note - at this point here the image may already be available
// in cache. if so, just display it. I have omitted that
// code for simplicity here.)
// so, right here, "possibly load with delay" the image
// exactly as shown in the code above .....
dispatch_after_secs_on_main(0.4, ^
...etc....
...etc....
}
Again this is all easily possible due to DLImageLoader which is amazing. It is an amazingly solid library.

UICollectionView scroll performance using AFNetworking to load images

I have read quite a few of the UICollectionView posts about poor scrolling, but none seem to directly apply or they are still unanswered.
I'm using AFNetworking to asynchronously load the images (95px squared) onto each cell and then when the images are scrolled into view again, the image is restored from cache (as verified by the response code given as 0 instead of 200).
Here's what I've tried:
Commented out weakCell.photoView.image = image; so the images aren't draw on screen and the scrolling was smoother (still stuttered a little during the HTTP get)
Removed all of the AFNetworking code from the cellForRowAtIndexPath method and the scrolling was much smoother (even with the custom cell shadows, etc. still being drawn on screen)
When I draw only the cell view (with the shadows) on screen, scrolling is very smooth for 100 cells. As soon as I start drawing the images on screen, scrolling is very poor on my device and it's even noticeable on the simulator. Instagram has very smooth scrolling for hundreds of cells on their profile view, so I'm trying to get close to their performance.
Are there any ways that I can improve any of my code below in order to improve scrolling performance?
Here is my cell code:
#import "PhotoGalleryCell.h"
#implementation PhotoGalleryCell
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
// Setup the background color, shadow, and border
self.backgroundColor = [UIColor colorWithWhite:0.25f alpha:1.0f];
self.layer.borderColor = [UIColor blackColor].CGColor;
self.layer.borderWidth = 0.5f;
self.layer.shadowColor = [UIColor blackColor].CGColor;
self.layer.shadowRadius = 3.0f;
self.layer.shadowOffset = CGSizeMake(0.0f, 2.0f);
self.layer.shadowOpacity = 0.5f;
// Make sure we rasterize for retina
self.layer.rasterizationScale = [UIScreen mainScreen].scale;
self.layer.shouldRasterize = YES;
// Add to the content view
self.photoView = [[UIImageView alloc] initWithFrame:self.bounds];
[self.contentView addSubview:self.photoView];
}
return self;
}
- (void)prepareForReuse
{
[super prepareForReuse];
self.photoView.image = nil;
self.largeImageURL = nil;
}
And here is my UICollectionView code:
#pragma mark - Collection View Delegates
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [zePhotos count];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
PhotoGalleryCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kPGPhotoCellIdentifier forIndexPath:indexPath];
// Get a reference to the image dictionary
NSDictionary *photoDict = [[zePhotos objectAtIndex:indexPath.row] objectForKey:#"image"];
// Asynchronously set the thumbnail view
__weak PhotoGalleryCell *weakCell = cell;
NSString *thumbnailURL = [[photoDict objectForKey:#"thumbnail"] objectForKey:#"url"];
NSURLRequest *photoRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:thumbnailURL]];
[cell.photoView setImageWithURLRequest:photoRequest
placeholderImage:nil
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
weakCell.photoView.image = image;
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
NSLog(#"Error retrieving thumbnail... %#", [error localizedDescription]);
}];
// Cache the large image URL in case they tap on this cell later
cell.largeImageURL = [[photoDict objectForKey:#"large"] objectForKey:#"url"];
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
[self performSegueWithIdentifier:#"showPhotoDetail" sender:self];
}
You could try adding a shadowPath to your cell init, it should improve performance, that's the code I used on one of my project to add a rounded shadowPath (see the UIBezierPath methods for more choice)
self.layer.shadowPath = [UIBezierPath bezierPathWithRoundedRect:self.frame.bounds
byRoundingCorners:UIRectCornerAllCorners
cornerRadii:CGSizeMake(10, 10)].CGPath;
Moreover if I remember correctly AFNetworking doesn't resize the image returned from the server, so it could have an impact on the quality of your image (despite the scale method you added to the UIImageView), I recommend dispatching the returned image to resize it if you want as so :
CGSize targetSize = cell.photoView.bounds.size;
[cell.photoView setImageWithURLRequest:photoRequest
placeholderImage:nil
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGFloat imageHeight = image.size.height;
CGFloat imageWidth = image.size.width;
CGSize newSize = weakCell.imageView.bounds.size;
CGFloat scaleFactor = targetSize.width / imageWidth;
newSize.height = imageHeight * scaleFactor;
UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0);
[image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
UIImage *small = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(),^{
weakCell.photoView.image = small;
});
});
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
NSLog(#"Error retrieving thumbnail... %#", [error localizedDescription]);
}];
Code inspection looks good, though I bet it is the compositing of the shadow which is adding a good deal to the lag. The way you figure out exactly what is causing the delay is to use the Time Profiler tool in Instruments. Here are the docs from Apple.
The problem is when you scroll quickly you're starting up hundreds of network requests at the same time. If you have the image cached, display it immediately. If you don't, only start the download when the table view slows down.
You can use something like this:
//Properties or Instance Variables
NSDate *scrollDateBuffer;
CGPoint scrollOffsetBuffer;
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
NSTimeInterval secondsSinceLastScroll = [[NSDate date] timeIntervalSinceDate:scrollDateBuffer];
CGFloat distanceSinceLastScroll = fabsf(scrollView.contentOffset.y - scrollOffsetBuffer.y);
BOOL slow = (secondsSinceLastScroll > 0 && secondsSinceLastScroll < 0.02);
BOOL small = (distanceSinceLastScroll > 0 && distanceSinceLastScroll < 1);
if (slow && small) {
[self loadImagesForOnscreenRows];
}
scrollDateBuffer = [NSDate date];
scrollOffsetBuffer = scrollView.contentOffset;
}
You will want to call loadImagesForOnscreenRows in other methods, like when new data comes in, viewWillAppear, and scrollViewDidScrollToTop.
Here's an example implementation of loadImagesForOnscreenRows:
- (void)loadImagesForOnscreenRows
{
#try {
for (UITableViewCell *cell in self.tableView.visibleCells) {
// load your images
NSURLRequest *photoRequest = …;
if (photoRequest) {
[cell.photoView setImageWithURLRequest:…];
}
}
}
#catch (NSException *exception) {
NSLog(#"Exception when loading table cells: %#", exception);
}
}
I have this in a try/catch block because in my experience [UITableView -visibleCells] isn't reliable - it occasionally returns deallocated cells or cells without a superview. If you make sure this method is only called when the table is not scrolling quickly, it shouldn't impact scroll performance too much.
Also, note that the AFNetworking UIImageView category doesn't expose the cache object. You'll need to modify it slightly to check if you already have an image cached; this answer should point you in the right direction.

Resources