UICollection View Scroll lag with SDWebImage - ios

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.

Related

iOS UIImageView memory not getting deallocated on ARC

I want to animate an image view in circular path and on click of image that image view need to change the new image. My problem is the images i allocated to the image view is not deallocated. And app receives memory warning and crashed. I surfed and tried lot of solutions for this problem but no use. In my case i need to create all ui components from Objective c class. Here am posting the code for creating image view and animation.
#autoreleasepool {
for(int i= 0 ; i < categories.count; i++)
{
NSString *categoryImage = [NSString stringWithFormat:#"%#_%ld.png",[categories objectAtIndex:i],(long)rating];
if (paginationClicked) {
if([selectedCategories containsObject:[categories objectAtIndex:i]]){
categoryImage = [NSString stringWithFormat:#"sel_%#",categoryImage];
}
}
UIImageView *imageView = [[UIImageView alloc] init];
imageView.image = [self.mySprites objectForKey:categoryImage];
imageView.contentMode = UIViewContentModeScaleAspectFit;
imageView.clipsToBounds = NO;
[imageView sizeToFit];
imageView.accessibilityHint = [categories objectAtIndex:i];
// imageView.frame = CGRectMake(location.x+sin(M_PI/2.5)*(self.view.frame.size.width*1.5),location.y+cos(M_PI/2.5)*(self.view.frame.size.width*1.5) , 150, 150);
imageView.userInteractionEnabled = YES;
imageView.multipleTouchEnabled = YES;
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(categoryTapGestureCaptured:)];
singleTap.numberOfTapsRequired = 1;
[imageView addGestureRecognizer:singleTap];
[categoryView addSubview:imageView];
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
UIBezierPath *path = [UIBezierPath bezierPath];
[path addArcWithCenter:location
radius:self.view.frame.size.width*1.5
startAngle:0.8
endAngle:-0.3+(0.1*(i+1))
clockwise:NO];
animation.path = path.CGPath;
imageView.center = path.currentPoint;
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
animation.duration = 1+0.25*i;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
// Apply it
[imageView.layer addAnimation:animation forKey:#"animation.trash"];
}
}
And this is the code to change the image on click.
for (UIImageView *subview in subviews) {
NSString *key = [NSString stringWithFormat:#"%#_%ld.png",subview.accessibilityHint,(long)rating];
if ([SelectedCategory isEqualToString:subview.accessibilityHint]) {
NSString *tempSubCategory = [categoryObj objectForKey:SelectedCategory];
if([selectedCategories containsObject:SelectedCategory]){
subview.image = [self.mySprites objectForKey:key];
[selectedCategories removeObject:SelectedCategory];
if (tempSubCategory.length != 0) {
subCategoriesAvailable = subCategoriesAvailable-1;
}
[self showNoPagination:subCategoriesAvailable+2];
}else{
if(selectedCategories.count != 2){
key = [NSString stringWithFormat:#"sel_%#",key];
subview.image = [self.mySprites objectForKey:key];
[selectedCategories addObject:SelectedCategory];
if ([SelectedCategory isEqualToString:#"Other"]) {
[self showCommentDialog];
}else{
if (tempSubCategory.length != 0) {
subCategoriesAvailable = subCategoriesAvailable+1;
}
[self showNoPagination:subCategoriesAvailable+2];
}
}
}
[self disableCategories];
break;
}
}
And i don't know what am doing wrong here. I tried nullifying on for loop but no use.
Code which i used for removing the image view
UIView *categoryView = [self.view viewWithTag:500];
NSArray *subviews = [categoryView subviews];
for (UIImageView *subview in subviews) {
if(![selectedCategories containsObject:subview.accessibilityHint]){
[subview removeFromSuperview];
subview.image = Nil;
}
}
Adding sprite reader code for reference
#import "UIImage+Sprite.h"
#import "XMLReader.h"
#implementation UIImage (Sprite)
+ (NSDictionary*)spritesWithContentsOfFile:(NSString*)filename
{
CGFloat scale = [UIScreen mainScreen].scale;
NSString* file = [filename stringByDeletingPathExtension];
if ([[UIScreen mainScreen] respondsToSelector:#selector(displayLinkWithTarget:selector:)] &&
(scale == 2.0))
{
file = [NSString stringWithFormat:#"%##2x", file];
}
NSString* extension = [filename pathExtension];
NSData* data = [NSData dataWithContentsOfFile:[NSString stringWithFormat:#"%#.%#", file,extension]];
NSError* error = nil;
NSDictionary* xmlDictionary = [XMLReader dictionaryForXMLData:data error:&error];
NSDictionary* xmlTextureAtlas = [xmlDictionary objectForKey:#"TextureAtlas"];
UIImage* image = [UIImage imageWithContentsOfFile:[NSString stringWithFormat:#"%#.%#", file,[[xmlTextureAtlas objectForKey:#"imagePath"]pathExtension]]];
CGSize size = CGSizeMake([[xmlTextureAtlas objectForKey:#"width"] integerValue],
[[xmlTextureAtlas objectForKey:#"height"] integerValue]);
if (!image || CGSizeEqualToSize(size, CGSizeZero)) return nil;
CGImageRef spriteSheet = [image CGImage];
NSMutableDictionary* tempDictionary = [[NSMutableDictionary alloc] init];
NSArray* xmlSprites = [xmlTextureAtlas objectForKey:#"sprite"];
for (NSDictionary* xmlSprite in xmlSprites)
{
CGRect unscaledRect = CGRectMake([[xmlSprite objectForKey:#"x"] integerValue],
[[xmlSprite objectForKey:#"y"] integerValue],
[[xmlSprite objectForKey:#"w"] integerValue],
[[xmlSprite objectForKey:#"h"] integerValue]);
CGImageRef sprite = CGImageCreateWithImageInRect(spriteSheet, unscaledRect);
// If this is a #2x image it is twice as big as it should be.
// Take care to consider the scale factor here.
[tempDictionary setObject:[UIImage imageWithCGImage:sprite scale:scale orientation:UIImageOrientationUp] forKey:[xmlSprite objectForKey:#"n"]];
CGImageRelease(sprite);
}
return [NSDictionary dictionaryWithDictionary:tempDictionary];
}
#end
Please help me to resolve this. Thanks in advance.
It looks like all the images are being retained by the dictionary(assumption) self.mySprites, as you are loading them with the call imageView.image = [self.mySprites objectForKey:categoryImage];
If you loaded the images into the dictionary with +[UIImage imageNamed:], then the dictionary initially contains only the compressed png images. Images are decompressed from png to bitmap as they are rendered to the screen, and these decompressed images use a large amount of RAM (that's the memory usage you're seeing labeled "ImageIO_PNG_Data"). If the dictionary is retaining them, then the memory will grow every time you render a new one to the screen, as the decompressed data is held inside the UIImage object retained by the dictionary.
Options available to you:
Store the image names in the self.mySprites dictionary, and load the images on demand. You should be aware that +[UIImage imageNamed:] implements internal RAM caching to speed things up, so this might also cause memory issues for you if the images are big, as the cache doesn't clear quickly. If this is an issue, consider using +[UIImage imageWithContentsOfFile:], although it requires some additional code (not much), which doesn't cache images in RAM.
Re-implement self.mySprites as an NSCache. NSCache will start throwing things out when the memory pressure gets too high, so you'll need to handle the case that the image is not there when you expect it to be, and load it from disk (perhaps using the above techniques)
CAKeyframeAnimation inherits from CAPropertyAnimation which in tern is inherited from CAAnimation.
If you see the delegate of CAAnimation class, it is strongly referenced as written below as it is declared -
/* The delegate of the animation. This object is retained for the
* lifetime of the animation object. Defaults to nil. See below for the
* supported delegate methods. */
#property(strong) id delegate;
Now you have added the reference of animation on imageView.layer, doing so the reference of imageView.layer will be strongly retained by CAAnimation reference.
Also you have set
animation.removedOnCompletion = NO;
which won't remove the animation from layer on completion
So if you are done with a image view then first removeAllAnimations from its layer and then release the image view.
I think as the CAAnimation strongly refers the imageView reference(it would also have increased it's retain count) and this could be the reason that you have removed the imageView from superview, after which it's retain count would still not be zero, and so leading to a leak.
Is there any specific requirement to set
animation.removedOnCompletion = NO;
since, setting
animation.removedOnCompletion = YES;
could solve the issue related to memory leak.
Alternatively, to resolve memory leak issue you can remove the corresponding animation on corresponding imageView' layer, by implementing delegate of CAAnimation, like as shown below -
/* Called when the animation either completes its active duration or
* is removed from the object it is attached to (i.e. the layer). 'flag'
* is true if the animation reached the end of its active duration
* without being removed. */
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    if (flag) {
        NSLog(#"%#", #"The animation is finished. Do something here.");
    }
//Get the reference of the corresponding imageView
    [imageView.layer removeAnimationForKey:#"animation.trash"];
}
UIView *categoryView = [self.view viewWithTag:500];
NSArray *subviews = [categoryView subviews];
for (UIImageView *subview in subviews) {
if(![selectedCategories containsObject:subview.accessibilityHint]){
[subview.layer removeAnimationForKey:#"animation.trash"]; // either this
//or// [subview.layer removeAllAnimations]; //alternatively
[subview removeFromSuperview];
subview.image = Nil;
}
}

Global variable is not being updated fast enough 50% of the time

I have a photo taking app. When the user presses the button to take a photo, I set a global NSString variable called self.hasUserTakenAPhoto equal to YES. This works perfectly 100% of the time when using the rear facing camera. However, it only works about 50% of the time when using the front facing camera and I have no idea why.
Below are the important pieces of code and a quick description of what they do.
Here is my viewDidLoad:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
self.topHalfView.frame = CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height/2);
self.takingPhotoView.frame = CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height);
self.afterPhotoView.frame = CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height);
self.bottomHalfView.frame = CGRectMake(0, 240, self.view.bounds.size.width, self.view.bounds.size.height/2);
PFFile *imageFile = [self.message objectForKey:#"file"];
NSURL *imageFileURL = [[NSURL alloc]initWithString:imageFile.url];
imageFile = nil;
self.imageData = [NSData dataWithContentsOfURL:imageFileURL];
imageFileURL = nil;
self.topHalfView.image = [UIImage imageWithData:self.imageData];
//START CREATING THE SESSION
self.session =[[AVCaptureSession alloc]init];
[self.session setSessionPreset:AVCaptureSessionPresetPhoto];
self.inputDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error;
self.deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:self.inputDevice error:&error];
if([self.session canAddInput:self.deviceInput])
[self.session addInput:self.deviceInput];
_previewLayer = [[AVCaptureVideoPreviewLayer alloc]initWithSession:_session];
self.rootLayer = [[self view]layer];
[self.rootLayer setMasksToBounds:YES];
[_previewLayer setFrame:CGRectMake(0, 240, self.rootLayer.bounds.size.width, self.rootLayer.bounds.size.height/2)];
[_previewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
[self.rootLayer insertSublayer:_previewLayer atIndex:0];
self.videoOutput = [[AVCaptureVideoDataOutput alloc] init];
self.videoOutput.videoSettings = #{ (NSString *)kCVPixelBufferPixelFormatTypeKey : #(kCVPixelFormatType_32BGRA) };
[self.session addOutput:self.videoOutput];
dispatch_queue_t queue = dispatch_queue_create("MyQueue", NULL);
[self.videoOutput setSampleBufferDelegate:self queue:queue];
[_session startRunning];
}
The Important part of viewDidLoad starts where I left the comment of //START CREATING THE SESSION
I basically create the session and then start running it. I have set this view controller as a AVCaptureVideoDataOutputSampleBufferDelegate, so as soon as the session starts running then the method below starts being called as well.
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection
{
//Sample buffer data is being sent, but don't actually use it until self.hasUserTakenAPhoto has been set to YES.
NSLog(#"Has the user taken a photo?: %#", self.hasUserTakenAPhoto);
if([self.hasUserTakenAPhoto isEqualToString:#"YES"]) {
//Now that self.hasUserTakenAPhoto is equal to YES, grab the current sample buffer and use it for the value of self.image aka the captured photo.
self.image = [self imageFromSampleBuffer:sampleBuffer];
}
}
This code is receiving the video output from the camera every second, but I don't actually do anything with it until self.hasUserTakenAPhoto is equal to YES. Once that has a string value of YES, then I use the current sampleBuffer from the camera and place it inside my global variable called self.image
So, here is when self.hasUserTakenAPhoto is actually set to YES.
Below is my IBAction code that is called when the user presses the button to capture a photo. A lot happens when this code runs, but really all that matters is the very first statement of: self.hasUserTakenAPhoto = #"YES";
-(IBAction)stillImageCapture {
self.hasUserTakenAPhoto = #"YES";
[self.session stopRunning];
if(self.inputDevice.position == 2) {
self.image = [self selfieCorrection:self.image];
} else {
self.image = [self rotate:UIImageOrientationRight];
}
CGFloat widthToHeightRatio = _previewLayer.bounds.size.width / _previewLayer.bounds.size.height;
CGRect cropRect;
// Set the crop rect's smaller dimension to match the image's smaller dimension, and
// scale its other dimension according to the width:height ratio.
if (self.image.size.width < self.image.size.height) {
cropRect.size.width = self.image.size.width;
cropRect.size.height = cropRect.size.width / widthToHeightRatio;
} else {
cropRect.size.width = self.image.size.height * widthToHeightRatio;
cropRect.size.height = self.image.size.height;
}
// Center the rect in the longer dimension
if (cropRect.size.width < cropRect.size.height) {
cropRect.origin.x = 0;
cropRect.origin.y = (self.image.size.height - cropRect.size.height)/2.0;
NSLog(#"Y Math: %f", (self.image.size.height - cropRect.size.height));
} else {
cropRect.origin.x = (self.image.size.width - cropRect.size.width)/2.0;
cropRect.origin.y = 0;
float cropValueDoubled = self.image.size.height - cropRect.size.height;
float final = cropValueDoubled/2;
finalXValueForCrop = final;
}
CGRect cropRectFinal = CGRectMake(cropRect.origin.x, finalXValueForCrop, cropRect.size.width, cropRect.size.height);
CGImageRef imageRef = CGImageCreateWithImageInRect([self.image CGImage], cropRectFinal);
UIImage *image2 = [[UIImage alloc]initWithCGImage:imageRef];
self.image = image2;
CGImageRelease(imageRef);
self.bottomHalfView.image = self.image;
if ([self.hasUserTakenAPhoto isEqual:#"YES"]) {
[self.takingPhotoView setHidden:YES];
self.image = [self screenshot];
[_afterPhotoView setHidden:NO];
}
}
So basically the viewDidLoad method runs and the session is started, the session is sending everything the camera sees to the captureOutput method, and then as soon as the user presses the "take a photo" button we set the string value of self.hasUserTakenAPhoto to YES, the session stops, and since self.hasUserTakenAPhoto is equal to YES now, the captureOutput method places the very last camera buffer into the self.image object for me to use.
I just can't figure this out because like I said it works 100% of the time when using the rear facing camera. However, when using the front facing camera it only works 50% of the time.
I have narrowed the problem down to the fact that self.hasUserTakenAPhoto does not update to YES fast enough when using the ront facing camera, and I know because if you look in my 2nd code I posted it has the statement of NSLog(#"Has the user taken a photo?: %#", self.hasUserTakenAPhoto);.
When this works correctly, and the user has just pressed the button to capture a photo which also stops the session, the very last time that NSLog(#"Has the user taken a photo?: %#", self.hasUserTakenAPhoto); runs it will print with the correct value of YES.
However, when it doesn't work correctly and doesn't update fast enough, the very last time it runs it still prints to the log with a value of null.
Any ideas on why self.hasUserTakenAPhoto does not update fast enough 50% of the time whe using the front facing camera? Even if we can't figure that out, it doesn't matter. I just need help then coming up with an alternate solution to this.
Thanks for the help.
I think its a scheduling problem. At the return point of your methods
– captureOutput:didOutputSampleBuffer:fromConnection:
– captureOutput:didDropSampleBuffer:fromConnection:
add a CFRunLoopRun()

UITableView async image loading - not laying out until scroll

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
}];

Fast blurring for UITableViewCell contentView Background

I have made a UIViewController which conforms to the UITableViewDataSource and UITableViewDelegate protocol and has a UITableView as it's subview.
I have set the backgroundView property of the table to be a UIImageView in order to display an image as the background of the table.
In order to have custom spacings between the cells I made the row height larger than I wanted and customised the cell's contentView to be the size I wanted, making it look like there is extra space (Following this SO answer).
I wanted to add a blur to the cell so that the background was blurred and I did this through Brad Larson's GPUImage framework. This works fine however, since I want the background blur to update as it scrolls, the scroll becomes very laggy.
My code is:
//Gets called from the -scrollViewDidScroll:(UIScrollView *)scrollView method
- (void)updateViewBG
{
UIImage *superviewImage = [self snapshotOfSuperview:self.tableView];
UIImage* newBG = [self applyTint:self.tintColour image:[filter imageByFilteringImage:superviewImage]];
self.layer.contents = (id)newBG.CGImage;
self.layer.contentsScale = newBG.scale;
}
//Code to create an image from the area behind the 'blurred cell'
- (UIImage *)snapshotOfSuperview:(UIView *)superview
{
CGFloat scale = 0.5;
if (([UIScreen mainScreen].scale > 1 || self.contentMode == UIViewContentModeScaleAspectFill)) {
CGFloat blockSize = 12.0f/5;
scale = blockSize/MAX(blockSize * 2, floor(self.blurRadius));
}
UIGraphicsBeginImageContextWithOptions(self.bounds.size, YES, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(context, -self.frame.origin.x, -self.frame.origin.y);
NSArray *hiddenViews = [self prepareSuperviewForSnapshot:superview];
[superview.layer renderInContext:context];
[self restoreSuperviewAfterSnapshot:hiddenViews];
UIImage *snapshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return snapshot;
}
-(UIImage*)applyTint:(UIColor*)colour image:(UIImage*)inImage{
UIImage *newImage;
if (colour) {
UIGraphicsBeginImageContext(inImage.size);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGRect area = CGRectMake(0, 0, inImage.size.width, inImage.size.height);
CGContextScaleCTM(ctx, 1, -1);
CGContextTranslateCTM(ctx, 0, -area.size.height);
CGContextSaveGState(ctx);
CGContextClipToMask(ctx, area, inImage.CGImage);
[[colour colorWithAlphaComponent:0.8] set];
CGContextFillRect(ctx, area);
CGContextRestoreGState(ctx);
CGContextSetBlendMode(ctx, kCGBlendModeLighten);
CGContextDrawImage(ctx, area, inImage.CGImage);
newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
} else {
newImage = inImage;
}
return newImage;
}
Now for the question:
Is there a better way to add the blur? Maybe so that the layer doesn't have to be rendered each movement? iOS7's control centre/notification centre seem to be able to do this without any lagging.
Maybe with the GPUImageUIElement class? If so, how do I use this?
Another way I looked at was to create the blur on the background image initially and then crop just the areas I needed to use out, however I couldn't get this to work, since the images may or may not be the same size as the screen so the scaling was a problem (Using CGImageCreateWithImageInRect() and the rect being the cell's position on the table).
I also found out that I have to add the blur to the tableview itself with the frame being that of the cell, and the cell having a clear colour.
Thanks in advance
EDIT
Upon request, here is the code for the image cropping I attempted before:
- (void)updateViewBG
{
//self.bgImg is the pre-blurred image, -getContentViewFromCellFrame: is a convenience method to get just the content area from the whole cell (since the contentarea is smaller than the cell)
UIImage* bg = [self cropImage:self.bgImg
toRect:[LATableBlur getContentViewFromCellFrame:[self.tableView rectForRowAtIndexPath:self.cellIndexPath]]];
bg = [self applyTint:self.tintColour image:bg];
self.layer.contents = (id)bg.CGImage;
self.layer.contentsScale = bg.scale;
}
- (UIImage*)cropImage:(UIImage*)image toRect:(CGRect)frame
{
CGSize imgSize = [image size];
double heightRatio = imgSize.height/self.tableView.frame.size.height;
double widthRatio = imgSize.width/self.tableView.frame.size.width;
UIImage* cropped = [UIImage imageWithCGImage:CGImageCreateWithImageInRect(image.CGImage,
CGRectMake(frame.origin.x*widthRatio,
frame.origin.y*heightRatio,
frame.size.width*widthRatio,
frame.size.height*heightRatio))];
return cropped;
}
I managed to solve it with a solution I, at first, didn't think it would work.
Generating several blurred images is certainly not the solution as it costs a lot.
I used only one blurred image and cached it.
So I subclassed UITableViewCell :
#interface BlurredCell : UITableViewCell
#end
I implemented two class methods to access the cached images (blurred and normal ones)
+(UIImage *)normalImage
{
static dispatch_once_t onceToken;
static UIImage *_normalImage;
dispatch_once(&onceToken, ^{
_normalImage = [UIImage imageNamed:#"bg.png"];
});
return _normalImage;
}
I used REFrostedViewController's category on UIImage to generate the blurred image
+(UIImage *)blurredImage
{
static dispatch_once_t onceToken;
static UIImage *_blurredImage;
dispatch_once(&onceToken, ^{
_blurredImage = [[UIImage imageNamed:#"bg.png"] re_applyBlurWithRadius:BlurredCellBlurRadius
tintColor:[UIColorcolorWithWhite:1.0f
alpha:0.4f]
saturationDeltaFactor:1.8f
maskImage:nil];
});
return _blurredImage;
}
In order to have the effect of blurred frames inside the cell but still see the non blurred image on the sides, I used to scroll views.
One with an image view with the normal image and the other one with an image view with the blurred image. I set the content size to be the size of the image and the contentOffset will be set through an interface.
So the table view ends up with each cell holding the whole background image but cropping it at certain offset and still showing the entire image
#implementation BlurredCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
// Initialization code
[self.contentView addSubview:self.normalScrollView];
[self.contentView addSubview:self.blurredScrollView];
}
return self;
}
-(UIScrollView *)normalScrollView
{
if (!_normalScrollView) {
_normalScrollView = [[UIScrollView alloc] initWithFrame:self.bounds];
_normalScrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_normalScrollView.scrollEnabled = NO;
UIImageView *imageView =[[UIImageView alloc] initWithFrame:[UIScreen mainScreen].bounds];
imageView.contentMode = UIViewContentModeScaleToFill;
imageView.image = [BlurredCell normalImage];
_normalScrollView.contentSize = imageView.frame.size;
[_normalScrollView addSubview:imageView];
}
return _normalScrollView;
}
-(UIScrollView *)blurredScrollView
{
if (!_blurredScrollView) {
_blurredScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(BlurredCellPadding, BlurredCellPadding,
self.bounds.size.width - 2.0f * BlurredCellPadding,
self.bounds.size.height - 2.0f * BlurredCellPadding)];
_blurredScrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_blurredScrollView.scrollEnabled = NO;
_blurredScrollView.contentOffset = CGPointMake(BlurredCellPadding, BlurredCellPadding);
UIImageView *imageView =[[UIImageView alloc] initWithFrame:[UIScreen mainScreen].bounds];
imageView.contentMode = UIViewContentModeScaleToFill;
imageView.image = [BlurredCell blurredImage];
_blurredScrollView.contentSize = imageView.frame.size;
[_blurredScrollView addSubview:imageView];
}
return _blurredScrollView;
}
-(void)setBlurredContentOffset:(CGFloat)offset
{
self.normalScrollView.contentOffset = CGPointMake(self.normalScrollView.contentOffset.x, offset);
self.blurredScrollView.contentOffset = CGPointMake(self.blurredScrollView.contentOffset.x, offset + BlurredCellPadding);
}
#end
setBlurredContentOffset: should be called each time the table view's content offset changes.
So in the table view delegate's implementation (the view controller) we do it in those two methods :
// For the first rows
-(void)tableView:(UITableView *)tableView willDisplayCell:(BlurredCell *)cell
forRowAtIndexPath:(NSIndexPath *)indexPath
{
[cell setBlurredContentOffset:cell.frame.origin.y];
}
// Each time the table view is scrolled
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
for (BlurredCell *cell in [self.tableView visibleCells]) {
[cell setBlurredContentOffset:cell.frame.origin.y - scrollView.contentOffset.y];
}
}
Here is a complete working demo

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