UITableView still slow with blocks - objective-c

so i have a tableView in my app, which basically run the names and images of some user's friends from Facebook.
The problem is that when i drag the tableView up and down, it's slow and not smooth like it should be, although i use block to upload images from Facebook- so it's asynchrony.
Now, after i implement a block for upload images from Facebook, it is run faster then it was before the block- but still not smooth enough.
is anybody know how to deal with it?
here is some of my code:
cellForRowAtIndexPath:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:#"friendCell"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:#"friendCell"];
[friendsTableView reloadData];
}
//set data for user's friends images
NSURL *url;
NSString *stringID;
stringID = [[NSString alloc]initWithFormat:#"https://graph.facebook.com/%#/picture?type=square",facebookFriendsID[indexPath.row] ];
url = [NSURL URLWithString:stringID];
//Calling block function to hendle user's friends images from facebook
[self downloadImageWithURL:url :stringID :cell completionBlock:^(BOOL succeeded, UIImage *image) {
if (succeeded) {
cell.imageView.image = image;
}
}];
//For friends name...
cell.textLabel.text = facebookFriendsName[indexPath.row];
cell.textLabel.font = [UIFont fontWithName:#"Noteworthy" size:17.0f];
[cell setSelectionStyle:UITableViewCellSelectionStyleNone];
cell.imageView.layer.masksToBounds = YES;
cell.imageView.layer.cornerRadius = 25.0;
return cell;
}
And the downloadImage block:
//For upload the images from Facebook asynchrony- using block.
- (void)downloadImageWithURL:(NSURL *)url : (NSString *)string : (UITableViewCell *)cell completionBlock:(void (^)(BOOL succeeded, UIImage *image))completionBlock
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if ( !error )
{
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
completionBlock(YES,image);
} else{
completionBlock(NO,nil);
}
}];
}
Thanks! :)

Your code has 4 problems, all of which are contributing to the slowness:
Delete [friendsTableView reloadData]; - you should never reload the table from inside cellForRowAtIndexPath:.
You should not reference cell from inside your completion block:
[self downloadImageWithURL:url :stringID :cell completionBlock:^(BOOL succeeded, UIImage *image) {
if (succeeded) {
cell.imageView.image = image;
}
}];
By the time your image downloads, cell may have been reused and be displaying different content. This can cause the wrong avatar image to appear. Instead, use indexPath to get the current cell by calling UITableViewCell *currentCell = [tableView cellForRowAtIndexPath:indexPath]; and set the image on that.
You are creating a new network request every time a cell appears. If the user scrolls very fast, you will have lots of simultaneous network requests. You may want to start network requests in scrollViewDidScroll: instead, when the scroll view slows down.
You are not de-duplicating network requests. If a user scrolls up and down really fast, they will generate lots of network requests to the same URL. You need to cache already-downloaded images and use those if the network request has already been made.*
The SDWebImage library has already solved all of these problems; you may simply want to use it instead of reinventing the wheel. At the very least, it's worth reading their code: https://github.com/rs/SDWebImage
* - NSURLCache may be caching the images for you, depending on the server's cache-control headers. But even so, this cache is slow (it caches the NSData representation, not the UIImage representation), and NSURLConnection will not stop you from starting 4 identical requests simultaneously.

Also, pay attention, that you load your image data for second time from main thread in request completion block:
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
you need to use NSData *data parameter from completionHandler - ^(NSURLResponse *response, NSData *data, NSError *error)
UIImage *image = [UIImage imageWithData:data];
BUT, as Aaron Brager noted, you don't need reinventing the wheel instead of using ready solution, like SDWebImage. so, all it matter if only you going to clarify how it works

Related

How to implement lazy loading of image without using SDImageCache?

How to implement lazy loading of image without using below link:
https://github.com/rs/SDWebImage
Note: I don't want to use any third party tool. I want to do from my side.
If you don't want to use any library (some are MIT or public domain licensed, so I think the only reason not to use them is that you want to learn how to build it yourself).
Here is how to do it with a simple and effective way:
1 : Put a temporary placeholder image in your imageView.
2 : Get your image in background thread.
2-a : If you want the cache feature, search for a cached image.
2-b : If no cache feature or no cached image, get your image from its source.
2-c : If cache feature, save the image to cache.
3 : In main thread show your image in the imageView.
Pseudo Code : (I wrote it on the go, it is not meant to run and it may have errors, sorry for that).
-(void) lazilyLoadImageFromURL :(NSURL *)url{
imageView.image = [UIImage imageNamed:#"Placeholder.png];
if([self cachedImageAvailableForURL:url){
imageView.image= [self cachedImageForURL:url];
}
else{
NSOperationQueue *queue = [NSOperationQueue mainQueue];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:queue completionHandler:^(NSURLResponse * resp, NSData *data, NSError *error)
{
dispatch_async(dispatch_get_main_queue(),^
{
if ( error == nil && data )
{
UIImage *urlImage = [[UIImage alloc] initWithData:data];
imageView.image = urlImage;
[self saveImageInCache:image forURL:url];
}
});
}];
}
}
-(BOOL) cachedImageAvailableForURL:(NSURL*):url{
// check if there is a saved cached image for this url
}
-(UIImage *) cachedImageForURL:(NSURL*):url{
// returns the cached image for that url
}
-(void) saveImageInCache:(UIImage*) image forURL:(NSURL*)url{
// saves the image in cache for the url
}
Of course, this is only ONE POSSIBLE WAY to do it. I tried to make it simple, but there are plenty better and more complicated ways to do it.
Try it:
NSURL *imageURL = [NSURL URLWithString:#"www...."];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
dispatch_async(dispatch_get_main_queue(), ^{
// Update the UI
self.imgVWprofile.image=[UIImage imageWithData:imageData];
});
});

Collectionview cell images not appearing in the correct order

What I have is an array of url's that point to images and a corresponding array of the names of the images
For example
['www.pictures/dog.jpg', 'www.pictures/lamp.jpg', 'www.pictures/piano.jpg']
['dog', 'lamp', 'piano']
I want each cell to have a the picture with the corresponding word. My problem is that the pictures almost always appear out of order. How I have it now, the words appear in order.
Anyone have an idea of how to get my images appear in order
- (photoCollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
photoCollectionViewCell *photoCell = [collectionView dequeueReusableCellWithReuseIdentifier:#"photoCell" forIndexPath:indexPath];
if(self.photoIndex == (self.totalPhotos)) {
self.photoIndex = 0;
}
NSURL *url = [NSURL URLWithString:self.imageURLArray[self.photoIndex]];
NSString *word = self.phraseWordsArray[self.photoIndex];
photoCell.photoImageView.image = [UIImage imageNamed:#"icn_default"];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
photoCell.photoImageView.image = image;
photoCell.photoLabel.text = word;
});
});
self.photoIndex++;
return photoCell;
}
When you are using dispatch_queue_t to request data, iOS system would perform the queue randomly since the best performance.
therefore your order is not what you expected.
reference:
https://developer.apple.com/library/prerelease/ios/documentation/Performance/Reference/GCD_libdispatch_Ref/index.html

Images from url in UITableViewCell loads only when i scroll the table view

Images downloaded from url displays image in UITableViewCell only when i start scrolling the UITableView. Now i am using following code to load image from url:
NSURL* url = [NSURL URLWithString:imageURL];
NSURLRequest* request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse * response,
NSData * data,
NSError * error) {
if (!error){
NSImage* image = [[NSImage alloc] initWithData:data];
// do whatever you want with image
}
}];
Thanks in advance.
You shouldn't be getting your images in - (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath you should create a utility class that acts as backing datastore. As you code stands now you could conceivable be re-requesting the image multiple times as the cell is scrolled into and out of view.
I Strongly recommend SDWebImage, which is very simple and powerful.
You can use setImageWithURL category , simply set image for imageView. For example
[cell.imageView setImageWithURL:[NSURL URLWithString:#"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]];`
And you can simply save your image cache by [[SDImageCache sharedImageCache] storeImage:myImage forKey:myImageCacheKey] and simply query your image cache by [[SDImageCache sharedImageCache] queryDiskCacheForKey:myImageCacheKey done:^doneBlock];
Very simple and powerful, for more details checkout the here SDWebImage;

How to fix a slow scrolling table view

I have a table view that's scrolling slowly. Does anyone know why that might be?
There is an image for each row, but even after the images are loaded it still stutters and scrolls slowly.
thanks for any help
here's my code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleTableIdentifier = #"SimpleTableCell";
SimpleTableCell *cell = (SimpleTableCell *)[tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"SimpleTableCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
}
// Get item from tableData
NSDictionary *item = (NSDictionary *)[displayItems objectAtIndex:indexPath.row];
// display the youdeal deal image
photoString = [item objectForKey:#"image"];
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:photoString]]];
cell.titleLabel.text = [item objectForKey:#"supercat"];
cell.descriptionLabel.text = [item objectForKey:#"title"];
NSString *convertedLeftCount = [NSString stringWithFormat:#"%#",[item objectForKey:#"left_count"]];
cell.amountLabel.text = convertedLeftCount;
cell.thumbnailImageView.image = image;
cell.priceLabel.text = [item objectForKey:#"cat"];
return cell;
}
It's due to the image loading mechanism you used.
You are loading the image from url in the main thread. That's why the UI is blocked for some time also the dataWithContentsOfURL: is a synchronous call. So the UI will respond after getting the image data.
Apple states that the time taking processes like webrequest,parsing huge data etc must be done on other threads rather than main thread.
And all UI related tasks must be done on main thread.
Solutions:
Request the image in background thread, not in main thread.
Cache the image once you get it
Source code and Third Party Libraries
Here are some links which will help you to understand the basic idea of loaing image using asynchronous methods
LazyTableImages
HJCache
SDWebImage
The images are getting loaded every time a cell is loaded, because the imageWithData: doesn't use any cache.
Edit: I saw a comment that suggests loading images asynchronously. You already have your custom class for each cell so it should be easy to do it. If it were an answer I'd vote it up
I think You are trying to say this.
NSURL* url = [NSURL URLWithString:#"http://www.YourImageUrl.com"];
NSURLRequest* request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse * response,
NSData * data,
NSError * error) {
if (!error){
UIImage* image = [[UIImage alloc] initWithData:data];
// Now workout with the image
}
}];
This will make the asynchronous call, and load the images after tableview loaded i.e when the image load complete the image will show but the table will be loaded when the table view is needed to load.

Reduce lag with UITableView and GCD

I have a UITableView consisting of roughly 10 subclassed UITableViewCells named TBPostSnapCell. Each cell, when initialised, sets two of its variables with UIImages downloaded via GCD or retrieved from a cache stored in the user's documents directory.
For some reason, this is causing a noticeable lag on the tableView and therefore disrupting the UX of the app & table.
Please can you tell me how I can reduce this lag?
tableView... cellForRowAtIndexPath:
if (post.postType == TBPostTypeSnap || post.snaps != nil) {
TBPostSnapCell *snapCell = (TBPostSnapCell *) [tableView dequeueReusableCellWithIdentifier:snapID];
if (snapCell == nil) {
snapCell = [[[NSBundle mainBundle] loadNibNamed:#"TBPostSnapCell" owner:self options:nil] objectAtIndex:0];
[snapCell setPost:[posts objectAtIndex:indexPath.row]];
[snapCell.bottomImageView setImage:[UIImage imageNamed:[NSString stringWithFormat:#"%d", (indexPath.row % 6) +1]]];
}
[snapCell.commentsButton setTag:indexPath.row];
[snapCell.commentsButton addTarget:self action:#selector(comments:) forControlEvents:UIControlEventTouchDown];
[snapCell setSelectionStyle:UITableViewCellSelectionStyleNone];
return snapCell;
}
TBSnapCell.m
- (void) setPost:(TBPost *) _post {
if (post != _post) {
[post release];
post = [_post retain];
}
...
if (self.snap == nil) {
NSString *str = [[_post snaps] objectForKey:TBImageOriginalURL];
NSURL *url = [NSURL URLWithString:str];
[TBImageDownloader downloadImageAtURL:url completion:^(UIImage *image) {
[self setSnap:image];
}];
}
if (self.authorAvatar == nil) {
...
NSURL *url = [[[_post user] avatars] objectForKey:[[TBForrstr sharedForrstr] stringForPhotoSize:TBPhotoSizeSmall]];
[TBImageDownloader downloadImageAtURL:url completion:^(UIImage *image) {
[self setAuthorAvatar:image];
}];
...
}
}
TBImageDownloader.m
+ (void) downloadImageAtURL:(NSURL *)url completion:(TBImageDownloadCompletion)_block {
if ([self hasWrittenDataToFilePath:filePathForURL(url)]) {
[self imageForURL:filePathForURL(url) callback:^(UIImage * image) {
_block(image); //gets UIImage from NSDocumentsDirectory via GCD
}];
return;
}
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_async(queue, ^{
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
dispatch_async(dispatch_get_main_queue(), ^{
[self writeImageData:UIImagePNGRepresentation(image) toFilePath:filePathForURL(url)];
_block(image);
});
});
}
First thing to try is converting DISPATCH_QUEUE_PRIORITY_HIGH (aka ONG MOST IMPORTANT WORK EVER FORGET EVERYTHING ELSE) to something like DISPATCH_QUEUE_PRIORITY_LOW.
If that doesn't fix it you could attempt to do the http traffic via dispatch_sources, but that is a lot of work.
You might also just try to limit the number of in flight http fetches with a semaphore, the real trick will be deciding what the best limit is as the "good" number will depend on the network, your CPUs, and memory pressure. Maybe benchmark 2, 4, and 8 with a few configurations and see if there is enough pattern to generalize.
Ok, lets try just one, replace the queue = ... with:
static dispatch_once_t once;
static dispatch_queue_t queue = NULL;
dispatch_once(&once, ^{
queue = dispatch_queue_create("com.blah.url-fetch", NULL);
});
Leave the rest of the code as is. This is likely to be the least sputtery, but may not load the images very fast.
For the more general case, rip out the change I just gave you, and we will work on this:
dispatch_async(queue, ^{
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
dispatch_async(dispatch_get_main_queue(), ^{
[self writeImageData:UIImagePNGRepresentation(image) toFilePath:filePathForURL(url)];
_block(image);
});
});
Replacing it with:
static dispatch_once_t once;
static const int max_in_flight = 2; // Also try 4, 8, and maybe some other numbers
static dispatch_semaphore_t limit = NULL;
dispatch_once(&once, ^{
limit = dispatch_semaphore_create(max_in_flight);
});
dispatch_async(queue, ^{
dispatch_semaphore_wait(limit, DISPATCH_TIME_FOREVER);
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
// (or you might want the dispatch_semaphore_signal here, and not below)
dispatch_async(dispatch_get_main_queue(), ^{
[self writeImageData:UIImagePNGRepresentation(image) toFilePath:filePathForURL(url)];
_block(image);
dispatch_semaphore_signal(limit);
});
});
NOTE: I haven't tested any of this code, even to see if it compiles. As written it will only allow 2 threads to be executing the bulk of the code in your two nested blocks. You might want to move the dispatch_semaphore_signal up to the commented line. That will limit you to two fetches/image creates, but they will be allowed to overlap with writing the image data to a file and calling your _block callback.
BTW you do a lot of file I/O which is faster on flash then any disk ever was, but if you are still looking for performance wins that might be another place to attack. For example maybe keeping the UIImage around in memory until you get a low memory warning and only then writing them to disk.