37

I'm developing an iOS 4 application with iOS 5.0 SDK and XCode 4.2.

I have to show some post blogs into a UITableView. When I have retreived all web service data, I use this method to create an UITableViewCell:

- (BlogTableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString* cellIdentifier = @"BlogCell";

    BlogTableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];

    if (cell == nil)
    {
        NSArray* topLevelObjects =  [[NSBundle mainBundle] loadNibNamed:@"BlogTableViewCell" owner:nil options:nil];

        for(id currentObject in topLevelObjects)
        {
            if ([currentObject isKindOfClass:[BlogTableViewCell class]])
            {
                cell = (BlogTableViewCell *)currentObject;
                break;
            }
        }
    }

    BlogEntry* entry = [blogEntries objectAtIndex:indexPath.row];

    cell.title.text = entry.title;
    cell.text.text = entry.text;
    cell.photo.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:entry.photo]]];

    return cell;
}

But this line:

cell.photo.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:entry.photo]]];

it is so slow (entry.photo has a http url).

Is there any way to load that image asynchronously? I think it is difficult because tableView:cellForRowAtIndexPath is called very often.

VansFannel
  • 45,055
  • 107
  • 359
  • 626

12 Answers12

81

I wrote a custom class to do just this, using blocks and GCD:

WebImageOperations.h

#import <Foundation/Foundation.h>

@interface WebImageOperations : NSObject {
}

// This takes in a string and imagedata object and returns imagedata processed on a background thread
+ (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage;
@end

WebImageOperations.m

#import "WebImageOperations.h"
#import <QuartzCore/QuartzCore.h>

@implementation WebImageOperations


+ (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage
{
    NSURL *url = [NSURL URLWithString:urlString];

    dispatch_queue_t callerQueue = dispatch_get_current_queue();
    dispatch_queue_t downloadQueue = dispatch_queue_create("com.myapp.processsmagequeue", NULL);
    dispatch_async(downloadQueue, ^{
        NSData * imageData = [NSData dataWithContentsOfURL:url];

        dispatch_async(callerQueue, ^{
            processImage(imageData);
        });
    });
    dispatch_release(downloadQueue);
}

@end

And in your ViewController

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{

    // Pass along the URL to the image (or change it if you are loading there locally)
    [WebImageOperations processImageDataWithURLString:entry.photo andBlock:^(NSData *imageData) {
    if (self.view.window) {
        UIImage *image = [UIImage imageWithData:imageData];

        cell.photo.image = image;
    }

    }];
}

It is very fast and will load the images without affecting the UI or scrolling speed of the TableView.

*** Note - This example assumes ARC is being used. If not, you will need to manage your own releases on objects)

Abdul Saleem
  • 10,098
  • 5
  • 45
  • 45
LJ Wilson
  • 14,445
  • 5
  • 38
  • 62
  • How do you prevent image re-downloading in this case? (I mean every time a cell is to be reused the method will fire, and it will re-download the data from server right?) – Alladinian Mar 20 '12 at 12:29
  • 1
    Just add the imagedata to an array and check for the existance of the imagedata in the array before loading it async. I will cook you something up, if you need me to (but it will be later today) – LJ Wilson Mar 20 '12 at 12:59
  • Oh no, no trouble I am not having any issues with that, I just wanted to make sure that I didn't miss anything in your example :) cheers – Alladinian Mar 20 '12 at 13:00
  • Is the example what you were looking for? – LJ Wilson Mar 20 '12 at 13:07
  • 4
    This is a good answer to the question posed. It should have at least a couple more votes. – Mark Mar 20 '12 at 13:12
  • 6
    By the way, in your completion block, you should _not_ just update the cell! You have no assurance that the cell hasn't subsequently scrolled off the screen and thus been dequeued and reused for another row of your table (important if you're scrolling super quickly or have slow network connection). You should be calling the tableview's `cellForRowAtIndexPath` (not to be confused with the table view controller's method of the same name) and make sure it's not `nil`. Thus `CustomCell *updateCell = [tableView cellForRowAtIndexPath:indexPath]; if (updateCell) updateCell.photo.image = ...;` – Rob Mar 09 '13 at 14:29
  • @ElJay You said "Just add the imagedata to an array and check for the existance of the imagedata in the array before loading it async.": Two refinements to that: 1. I'd suggest `NSCache` rather than array (so you can apply some reasonable limit to how many images you'll cache in RAM); 2. You might also want to cache to users' ** `/Library/Caches` folder in case RAM cache is purged, you won't have to go back to network. – Rob Mar 09 '13 at 14:43
  • One final observation: This is creating a separate serial queues (all with the same name) for each image! That's a little inefficient, and if that's your intent, you should just use global queue. But, more critically, given that you can have only a limited number of concurrent requests with a server, if you submit a lot of requests this way, some might time out and fail. Use `NSOperationQueue` with `maxConcurrentOperationCount` if you want concurrency and not risk timeouts. – Rob Mar 09 '13 at 16:22
  • 2
    dispatch_get_current_queue is deprecated in iOS 6. – bbrame Sep 17 '13 at 17:44
  • I am now using AFNetworking's method for loading images asynchronously. It has options for turning caching on/off and is really fast. Since I was already using AFNetworking, adopting the setImageWithURL methods was a no-brainer. – LJ Wilson Nov 18 '13 at 14:15
  • @LJWilson Its really working fine. One question : before loading image in to list screen if i go to detail screen than back again to list then images are not loading in background (which are remining). i have to refresh list all time. could we sort out this ? – Hitarth Dec 26 '14 at 10:20
  • Great work it helped me but delete the releasequeue as it is developed for ARC – ashokdy Jan 08 '15 at 10:20
52

In iOS 6 and later dispatch_get_current_queue gives deprecation warnings.

Here is an alternative that is a synthesis of the @ElJay answer above and the article by @khanlou here.

Create a category on UIImage:

UIImage+Helpers.h

@interface UIImage (Helpers)

+ (void) loadFromURL: (NSURL*) url callback:(void (^)(UIImage *image))callback;

@end

UIImage+Helpers.m

#import "UIImage+Helpers.h"

@implementation UIImage (Helpers)

+ (void) loadFromURL: (NSURL*) url callback:(void (^)(UIImage *image))callback {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
    dispatch_async(queue, ^{
        NSData * imageData = [NSData dataWithContentsOfURL:url];
        dispatch_async(dispatch_get_main_queue(), ^{
            UIImage *image = [UIImage imageWithData:imageData];
            callback(image);
        });
    });
}

@end
Ja͢ck
  • 170,779
  • 38
  • 263
  • 309
bbrame
  • 18,031
  • 10
  • 35
  • 52
28

Take a look at SDWebImage:

https://github.com/rs/SDWebImage

It's a fantastic set of classes that handle everything for you.

Tim

tarmes
  • 15,366
  • 10
  • 53
  • 87
  • 3
    If someone needs resizing/cropping capabilities, I integrated this library with UIImage+Resize library. Check it out in https://github.com/toptierlabs/ImageCacheResize – Tony Oct 28 '12 at 02:38
  • this library has problem if you write info in NSTemporaryDirectory parallel to this. Any other solution? – jose920405 Sep 09 '15 at 14:10
6

Swift:

extension UIImage {

    // Loads image asynchronously
    class func loadFromURL(url: NSURL, callback: (UIImage)->()) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), {

            let imageData = NSData(contentsOfURL: url)
            if let data = imageData {
                dispatch_async(dispatch_get_main_queue(), {
                    if let image = UIImage(data: data) {
                        callback(image)
                    }
                })
            }
        })
    }
}

Usage:

// First of all remove the old image (required for images in cells)
imageView.image = nil 

// Load image and apply to the view
UIImage.loadFromURL("http://...", callback: { (image: UIImage) -> () in
     self.imageView.image = image
})
Alexander Volkov
  • 7,904
  • 1
  • 47
  • 44
5

Swift 4 | Async loading of image

Make a new class named ImageLoader.swift

import UIKit

class ImageLoader {

var cache = NSCache<AnyObject, AnyObject>()

class var sharedInstance : ImageLoader {
    struct Static {
        static let instance : ImageLoader = ImageLoader()
    }
    return Static.instance
}

func imageForUrl(urlString: String, completionHandler:@escaping (_ image: UIImage?, _ url: String) -> ()) {
        let data: NSData? = self.cache.object(forKey: urlString as AnyObject) as? NSData

        if let imageData = data {
            let image = UIImage(data: imageData as Data)
            DispatchQueue.main.async {
                completionHandler(image, urlString)
            }
            return
        }

    let downloadTask: URLSessionDataTask = URLSession.shared.dataTask(with: URL.init(string: urlString)!) { (data, response, error) in
        if error == nil {
            if data != nil {
                let image = UIImage.init(data: data!)
                self.cache.setObject(data! as AnyObject, forKey: urlString as AnyObject)
                DispatchQueue.main.async {
                    completionHandler(image, urlString)
                }
            }
        } else {
            completionHandler(nil, urlString)
        }
    }
    downloadTask.resume()
    }
}

To Use in your ViewController class:

ImageLoader.sharedInstance.imageForUrl(urlString: "https://www.logodesignlove.com/images/classic/apple-logo-rob-janoff-01.jpg", completionHandler: { (image, url) in
                if image != nil {
                    self.imageView.image = image
                }
            })
Akshay Sarda
  • 51
  • 1
  • 2
2

Considering Failure Case.

- (void) loadFromURL: (NSURL*) url callback:(void (^)(UIImage *image))callback {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
    dispatch_async(queue, ^{
        NSError * error = nil;
        NSData * imageData = [NSData dataWithContentsOfURL:url options:0 error:&error];
        if (error)
            callback(nil);

        dispatch_async(dispatch_get_main_queue(), ^{
            UIImage *image = [UIImage imageWithData:imageData];
            callback(image);
        });
    });
}
jose920405
  • 7,982
  • 6
  • 45
  • 71
2

Yes it's relatively easy. The idea is something like:

  1. Create a mutable array to hold your images
  2. Populate the array with placeholders if you like (or NSNull objects if you don't)
  3. Create a method to fetch your images asynchronously in the background
  4. When an image has arrived, swap your placeholder with the real image and do a [self.tableView reloadData]

I have tested this approach many times and gives great results. If you need any help / examples with the async part (I recommend to use gcd for that) let me know.

Alladinian
  • 34,483
  • 6
  • 89
  • 91
  • If the image is stored on a webserver, why spend the resources to download all the images when you only need the images for the displayed cells? – LJ Wilson Mar 20 '12 at 12:15
  • Mainly for caching reasons, but yes for a large set you could fetch only the images needed for display. My point was to stress out the general concept. – Alladinian Mar 20 '12 at 12:26
  • I would add that `reloadData` should be called from the main thread so that the views are updated almost immediately. Thanks for the tip! – Jonathan Lin Mar 26 '13 at 04:29
1

you can easily do it perfectly if you use the sample code provided by Apple for this purpose: the sample code : Lazy Image

Just look at the delegate rowforcell and add icondownloader files to you project.

The only change you have to do is to change apprecord object with your object.

Firas KADHUM
  • 51
  • 10
1

While SDWebImage and other 3rd party maybe great solutions, if you are not keen on using 3rd party APIs, you can develop your own solution too.

Refer to this tutorial about lazy loading which also talks about how should you model your data inside table view.

Nirav Bhatt
  • 6,940
  • 5
  • 45
  • 89
0

Use below code snippet to loading image into imageview

func imageDownloading() {

DispatchQueue.global().async {

    let url = URL(string: "http://verona-api.municipiumstaging.it/system/images/image/image/22/app_1920_1280_4.jpg")!

    do {

        let data = try Data(contentsOf: url)

        DispatchQueue.main.async {

        self.imageView.image = UIImage(data: data)

        }

    } catch {
        print(error.localizedDescription)
    }
}
}
ShigaSuresh
  • 1,598
  • 17
  • 19
0

AsyncImage can synchronously load and display an image. is officially introduced after iOS 15

cell.photo = AsyncImage(url: URL(string: entry.photo))
    .frame(width: 200, height: 200)

It also supports:

See more in doc

RY_ Zheng
  • 3,041
  • 29
  • 36
0

You'll probably have to subclass your UIImageView. I've recently made a simple project to explain this particular task - background asynchronous image loading - take a look at my project at GitHub. Specifically, look at KDImageView class.

Kyr Dunenkoff
  • 8,090
  • 3
  • 23
  • 21