setImage: forState: lagging the UI - objective-c

I have a tableview which runs some code for fetching a UIImage from NSData in a background thread, then in the foreground it calls [button setImage:image forState:UIControlStateNormal];.
The problem is that it freezes the UI for a split second on that line, until it's complete. This means that the UI freezes while it sets it as the image. Because the user is scrolling, this is very noticeable and jittery. Is there any way to avoid my UI from freezing like this?
dispatch_queue_t cellSetupQueue = dispatch_queue_create("Setup", NULL);
dispatch_async(cellSetupQueue, ^{
UIImage *image = [self.mediaDelegate largeThumbnailForMediaAtIndex:indexPath.row];
dispatch_async(dispatch_get_main_queue(), ^{
[self.thumbnailButton setImage:image forState:UIControlStateNormal];
});
});
dispatch_release(cellSetupQueue);

I'm guessing your images are JPGs. If so then UIImage will defer decoding the actual image until the first time it actually needs those pixels. Check out the ImageIO framework for more precise control on this. Here's a snippet I use:
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); // data is your image file NSData
if (source)
{
NSDictionary *dict = #{ (__bridge NSString *)kCGImageSourceShouldCache : #(YES) };
CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, (__bridge CFDictionaryRef)dict);
if (cgImage)
{
CFRelease(source);
return cgImage; // you can keep this reference so you dont have to decode again later
}
}
The important part is the kCGImageSourceShouldCache option. You can run this code on the background thread and pass the decoded image to the UI thread.

Related

App Crashes when too many images are in TableView

I have a table that I am populating with information stored in Runner Objects. Each Runner object has a property called fileName that corresponds to an image saved in the document directory.
If I start with an empty table, I can add runners and their photos just fine until I reach about 8 runners. When I try to add a photo for the 8th or so runner, the app crashes. It seems to be a memory issue, but I don't see how this can be when all I'm storing locally are references to images in the directory.
I pass a runner object to my set up cell method in my RunnerCell class and set up the cell like so:
if (currentRunner.fileName != nil) {
if ([manager fileExistsAtPath:fullPath]){
UIImage *originalImage = [[UIImage alloc] initWithContentsOfFile:fullPath];
if ([currentRunner.photoOrientation isEqual: #"portrait"]) {
CGImageRef imageRef = [originalImage CGImage];
UIImage *rotatedImage = [UIImage imageWithCGImage:imageRef scale:1.0 orientation:UIImageOrientationRight];
self.runnerImageView.image = rotatedImage;
} else {
self.runnerImageView.image = originalImage;
}
self.addPhotoLabel.hidden = true;
}
}
As per documentation states that you mustn't load image to a UITableViewCell on main thread. That is why your app is crashing.
Because when you load image on main thread it became too much laggy when you scroll because dequereusable cell reassign cell and again load image for upcoming indexpath's cell and until the image loads it block the main thread which cause laggy behaviour of tableview.
Please load your cached image in a background thread. use this code.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^(void) {
if ([currentRunner.photoOrientation isEqual: #"portrait"]) {
CGImageRef imageRef = [originalImage CGImage];
UIImage *rotatedImage = [UIImage imageWithCGImage:imageRef scale:1.0 orientation:UIImageOrientationRight];
dispatch_sync(dispatch_get_main_queue(), ^(void) {
self.runnerImageView.image = rotatedImage;
});
} else {
dispatch_sync(dispatch_get_main_queue(), ^(void) {
self.runnerImageView.image = originalImage;
});
}
});

UIScrollView loading images -makes scroller be slowly

I have a scroll view with many UIViews, and these views have UIImageViews-that i load images into, while scrolling.
So every page has 4 of these views,that are preloaded to scroller at start, and i load the next page images while scrolling.
I do that in another thread , and still, scroller not entirely scrolls fast, if you look good it has some tiny flicks .
here is the function that loads them :
-(void)loadMainImageToIndex:(int)index
{
NSDictionary *dic=[mainData objectAtIndex:index];
NSString *userImageUrl=[dic objectForKey:#"url"];
NSURL *userUrl=[NSURL URLWithString:userImageUrl];
[self downloadImageWithURL:userUrl completionBlock:^(BOOL succeeded, NSData *tdata)
{
if (succeeded)
{
//load back
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^
{
UIImage *image=[UIImage imageWithData:tdata scale:0.25];
if (image)
{
UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale);
[image drawAtPoint:CGPointZero];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
dispatch_async(dispatch_get_main_queue(), ^
{
UIImageView *view=[mainImagesArray objectAtIndex:index];
});
}
}];
The url is loaded Asynch.
Turned out that the views shadow cause this thing :
//self.layer.shadowOffset=CGSizeMake(6.0, 6.0);
//self.layer.shadowColor=[UIColor blackColor].CGColor;
//self.layer.shadowRadius = 3;
//self.layer.shadowOpacity = 0.7;
Removing this solves the problem.
self.layer.shouldRasterize = YES; //solve the problem,

Poor UICollectionView Scrolling Performance With UIImage

I have a UICollectionView in my app, and each cell is a UIImageView and some text labels. The problem is that when I have the UIImageViews displaying their images, the scrolling performance is terrible. It's nowhere near as smooth as the scrolling experience of a UITableView or even the same UICollectionView without the UIImageView.
I found this question from a few months ago, and it seems like an answer was found, but it's written in RubyMotion, and I don't understand that. I tried to see how to convert it to Xcode, but since I have never used NSCache either, it's a little hard to. The poster there also pointed to here about implementing something in addition to their solution, but I'm not sure where to put that code either. Possibly because I don't understand the code from the first question.
Would someone be able to help translate this into Xcode?
def viewDidLoad
...
#images_cache = NSCache.alloc.init
#image_loading_queue = NSOperationQueue.alloc.init
#image_loading_queue.maxConcurrentOperationCount = 3
...
end
def collectionView(collection_view, cellForItemAtIndexPath: index_path)
cell = collection_view.dequeueReusableCellWithReuseIdentifier(CELL_IDENTIFIER, forIndexPath: index_path)
image_path = #image_paths[index_path.row]
if cached_image = #images_cache.objectForKey(image_path)
cell.image = cached_image
else
#operation = NSBlockOperation.blockOperationWithBlock lambda {
#image = UIImage.imageWithContentsOfFile(image_path)
Dispatch::Queue.main.async do
return unless collectionView.indexPathsForVisibleItems.containsObject(index_path)
#images_cache.setObject(#image, forKey: image_path)
cell = collectionView.cellForItemAtIndexPath(index_path)
cell.image = #image
end
}
#image_loading_queue.addOperation(#operation)
end
end
Here is the code from the second question that the asker of the first question said solved the problem:
UIImage *productImage = [[UIImage alloc] initWithContentsOfFile:path];
CGSize imageSize = productImage.size;
UIGraphicsBeginImageContext(imageSize);
[productImage drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)];
productImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
Again, I'm not sure how/where to implement that.
Many thanks.
Here's the pattern I follow. Always load asynch and cache the result. Make no assumption about the state of the view when the asynch load finishes. I have a class that simplifies the loads as follows:
//
// ImageRequest.h
// This class keeps track of in-flight instances, creating only one NSURLConnection for
// multiple matching requests (requests with matching URLs). It also uses NSCache to cache
// retrieved images. Set the cache count limit with the macro in this file.
#define kIMAGE_REQUEST_CACHE_LIMIT 100
typedef void (^CompletionBlock) (UIImage *, NSError *);
#interface ImageRequest : NSMutableURLRequest
- (UIImage *)cachedResult;
- (void)startWithCompletion:(CompletionBlock)completion;
#end
//
// ImageRequest.m
#import "ImageRequest.h"
NSMutableDictionary *_inflight;
NSCache *_imageCache;
#implementation ImageRequest
- (NSMutableDictionary *)inflight {
if (!_inflight) {
_inflight = [NSMutableDictionary dictionary];
}
return _inflight;
}
- (NSCache *)imageCache {
if (!_imageCache) {
_imageCache = [[NSCache alloc] init];
_imageCache.countLimit = kIMAGE_REQUEST_CACHE_LIMIT;
}
return _imageCache;
}
- (UIImage *)cachedResult {
return [self.imageCache objectForKey:self];
}
- (void)startWithCompletion:(CompletionBlock)completion {
UIImage *image = [self cachedResult];
if (image) return completion(image, nil);
NSMutableArray *inflightCompletionBlocks = [self.inflight objectForKey:self];
if (inflightCompletionBlocks) {
// a matching request is in flight, keep the completion block to run when we're finished
[inflightCompletionBlocks addObject:completion];
} else {
[self.inflight setObject:[NSMutableArray arrayWithObject:completion] forKey:self];
[NSURLConnection sendAsynchronousRequest:self queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (!error) {
// build an image, cache the result and run completion blocks for this request
UIImage *image = [UIImage imageWithData:data];
[self.imageCache setObject:image forKey:self];
id value = [self.inflight objectForKey:self];
[self.inflight removeObjectForKey:self];
for (CompletionBlock block in (NSMutableArray *)value) {
block(image, nil);
}
} else {
[self.inflight removeObjectForKey:self];
completion(nil, error);
}
}];
}
}
#end
Now the cell (collection or table) update is fairly simple:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
NSURL *url = [NSURL URLWithString:#"http:// some url from your model"];
// note that this can be a web url or file url
ImageRequest *request = [[ImageRequest alloc] initWithURL:url];
UIImage *image = [request cachedResult];
if (image) {
UIImageView *imageView = (UIImageView *)[cell viewWithTag:127];
imageView.image = image;
} else {
[request startWithCompletion:^(UIImage *image, NSError *error) {
if (image && [[collectionView indexPathsForVisibleItems] containsObject:indexPath]) {
[collectionView reloadItemsAtIndexPaths:#[indexPath]];
}
}];
}
return cell;
}
In general bad scrolling behaviour for UICollectionViews or UITableViews happens because the cells are dequeued and constructed in the main thread by iOS. There is little freedom to precache cells or construct them in a background thread, instead they are dequeued and constructed as you scroll blocking the UI. (Personally I find this bad design by Apple all though it does simplify matters because you don't have to be aware about potential threading issues. I think they should have given a hook though to provide a custom implementation for a UICollectionViewCell/UITableViewCell pool which can handle dequeuing/reusing of cells.)
The most important causes for performance decrease are indeed related to image data and (in decreasing order of magnitude) are in my experience:
Synchronous calls to download image data: always do this asynchronously and call [UIImageView setImage:] with the constructed image when ready in the main thread
Synchronous calls to construct images from data on the local file system, or from other serialized data: do this asynchronously as well. (e.g. [UIImage imageWithContentsOfFile:], [UIImage imageWithData:], etc).
Calls to [UIImage imageNamed:]: the first time this image is loaded it is served from the file system. You may want to precache images (just by loading [UIImage imageNamed:] before the cell is actually constructed such that they can be served from memory immediately instead.
Calling [UIImageView setImage:] is not the fastest method either, but can often not be avoided unless you use static images. For static images it is sometimes faster to used different image views which you set to hidden or not depending on whether they should be displayed instead of changing the image on the same image view.
First time a cell is dequeued it is either loaded from a Nib or constructed with alloc-init and some initial layout or properties are set (probably also images if you used them). This causes bad scrolling behaviour the first time a cell is used.
Because I am very picky about smooth scrolling (even if it's only the first time a cell is used) I constructed a whole framework to precache cells by subclassing UINib (this is basically the only hook you get into the dequeuing process used by iOS). But that may be beyond your needs.
I had issues about UICollectionView scrolling.
What worked (almost) like a charm for me: I populated the cells with png thumbnails 90x90. I say almost because the first complete scroll is not so smooth, but never crashed anymore.
In my case, the cell size is 90x90.
I had many original png sizes before, and it was very choppy when png original size was greater than ~1000x1000 (many crashes on first scroll).
So, I select 90x90 (or the like) on the UICollectionView and display the original png's (no matter the size). hope it may help others.

ALAssetRepresentation fullResolutionImage never returns

I am writing a multithreaded application which needs to upload photos from the ALAssetsLibrary en masse in the background. So I have an NSOperation subclass which finds the appropriate ALAsset via the asset's URL and adds the image to an upload queue.
In the upload queue for the current ALAsset, I need to get the metadata from the image, but I've encountered a problem: both the -metadata and the -fullResolutionImage methods never return when they are called on the ALAssetRepresentation of the ALAsset. They simply hang there indefinitely. I tried printing the value of each of these methods in LLDB, but it hung the debugger up, and I ended up killing Xcode, signal 9 style. These methods are being called on a background queue.
I am testing these on an iPad 2. This is the method in which the ALAsset is added to the upload queue when it is found in the success block of -assetForURL:resultBlock:failureBlock:
- (void)addMediaToUploadQueue:(ALAsset *)media {
#autoreleasepool {
ALAssetRepresentation *defaultRepresentation = [media defaultRepresentation];
CGImageRef fullResolutionImage = [defaultRepresentation fullResolutionImage];
// Return if the user is trying to upload an image which has already been uploaded
CGFloat scale = [defaultRepresentation scale];
UIImageOrientation orientation = [defaultRepresentation orientation];
UIImage *i = [UIImage imageWithCGImage:fullResolutionImage scale:scale orientation:orientation];
if (![self isImageUnique:i]) return;
NSDictionary *imageDictionary = [self dictionaryForAsset:media withImage:i];
dispatch_async(self.background_queue, ^{
NSManagedObjectContext *ctx = [APPDELEGATE createManagedObjectContextForThread];
[ctx setUndoManager:nil];
[ctx performBlock:^{
ImageEntity *newImage = [NSEntityDescription insertNewObjectForEntityForName:#"ImageEntity"
inManagedObjectContext:ctx];
[newImage updateWithDictionary:imageDictionary
inManagedObjectContext:ctx];
[ctx save:nil];
[APPDELEGATE saveContext];
dispatch_async(dispatch_get_main_queue(), ^{
[self.fetchedResultsController performFetch:nil];
});
if (!currentlyUploading) {
currentlyUploading = YES;
[self uploadImage:newImage];
}
}];
});
}
}
I had a similar problem and I was tearing my hair out trying to figure it out.
Turns out while I had thought I setup a singleton for ALAssetsLibrary, my code was not calling it properly and some ALAssets were returning an empty 'fullResolutionImage'
In all of my NSLogs I must have missed the most important message from Xcode:
"invalid attempt to access ALAssetPrivate past the lifetime of its owning ALAssetsLibrary"
Follow this link
http://www.daveoncode.com/2011/10/15/solve-xcode-error-invalid-attempt-to-access-alassetprivate-past-the-lifetime-of-its-owning-alassetslibrary/
I hope that helps

how load many photos from url in background (asynchronous)

i have this method i use to load many images to scroll view, but when the images load from the url my scroll view is stuck and i can't understand how to load them in the background so the user will not feel it.
this method will call few times (8) in "for" cycle.
- (void)loadPhotosToLeftscroll{
//getting image information from JSON
NSMutableDictionary *photoDict;
photoDict = [leftPhotoArray lastObject];
//getting the photo
NSString *photoPath = [photoDict objectForKey:#"photos_path"];
NSLog(#"photo Path:%#",photoPath);
NSData * imageData = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:photoPath]];
UIImage *image = [UIImage imageWithData:imageData];
// use the image how you like, say, as your button background
//calculating the hight of next photo
UIImageView *leftImage = [leftBlockScroll.subviews lastObject];
//allocating photoView
UIImageView *photoView = [[UIImageView alloc]initWithFrame:CGRectMake(5 , leftImage.frame.origin.y + leftImage.frame.size.height+5, image.size.width/2, image.size.height/2 )];
photoView.userInteractionEnabled=YES;
[photoView.layer setMasksToBounds:YES];
[photoView.layer setCornerRadius:3];
//getting items list
NSDictionary *sh_items = [photoDict objectForKey:#"items"];
//adding image button
UIButton *imageOverButton = [UIButton buttonWithType:UIButtonTypeCustom];
imageOverButton.frame = CGRectMake(0, 0, photoView.frame.size.width, photoView.frame.size.height);
[imageOverButton addTarget:self action:#selector(LeftimagePressed:) forControlEvents:UIControlEventTouchUpInside];
[imageOverButton setTag:[leftPhotoArray count]-1];
[photoView addSubview:imageOverButton];
//adding sh button to imageView
[self addThe_sh_signs:sh_items To_ImageView:photoView];
//subViewing the image to the scrollView
[self insert_Image:image toImageView:photoView in_Scroll:leftBlockScroll];
//calclulating the position of next imageView in scroll.
nextLeftPhotoHight = photoView.frame.size.height + photoView.frame.origin.y + 5;
//calculating the hight of the highest scroll view in both.
leftBlockScroll.contentSize = CGSizeMake(160, [self theSizeOfScrollViewHight]);
rightBlocScroll.contentSize = CGSizeMake(160, [self theSizeOfScrollViewHight]);
isLoadindContant = NO;
[self.view reloadInputViews];
[leftBlockScroll reloadInputViews];
}
please do not send me to some link that trying to explain how to use the asynchronous.
Try to explain according the method you see here.
Im here for any question, that you need to ask to help me.
You will have to do it asynchronously in a proper way. I do not think there is any way around that. I subclassed an UIImageView object and placed many instances of it within the cells of a talbe (in your case within the scroll view). The subclass objects are initialized with an url and load their image asynchronously (with some caching so that the image is not loaded every time).
This tutorial helped me much in the beginning:
http://www.markj.net/iphone-asynchronous-table-image/
You will just have to adopt that to your scroll view. The underlaying principle remains the same.