Loading an image into UIImage asynchronously
Asked Answered
C

12

38

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.

Coating answered 20/3, 2012 at 11:37 Comment(0)
C
28

Take a look at SDWebImage:

https://github.com/rs/SDWebImage

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

Tim

Crompton answered 20/3, 2012 at 13:20 Comment(2)
If someone needs resizing/cropping capabilities, I integrated this library with UIImage+Resize library. Check it out in github.com/toptierlabs/ImageCacheResizeSaccharate
this library has problem if you write info in NSTemporaryDirectory parallel to this. Any other solution?Goeselt
S
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)

Sacrificial answered 20/3, 2012 at 12:12 Comment(12)
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?)Salk
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)Sacrificial
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 :) cheersSalk
Is the example what you were looking for?Sacrificial
This is a good answer to the question posed. It should have at least a couple more votes.Callup
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 = ...;Odum
@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' <Application_Home> /Library/Caches folder in case RAM cache is purged, you won't have to go back to network.Odum
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.Odum
dispatch_get_current_queue is deprecated in iOS 6.Primrose
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.Sacrificial
@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 ?Omniscience
Great work it helped me but delete the releasequeue as it is developed for ARCQuadruple
P
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
Primrose answered 17/9, 2013 at 17:53 Comment(5)
A category is really the way to go.Civics
how can I call the method in a class? UIImage *img = [UIImage loadFromURL:[NSURL URLWithString:@""] callback:<#^(UIImage *image)callback#>]; What should I put in the callback?Ewing
@Primrose please give me an answer!Ewing
Want to load two images simultaneously. It takes a long time, why?Pectase
@Ewing ... a sample implementation would be: [UIImage loadFromURL:_myNSURL callback:^(UIImage *image){ _myImageView.image = image; _mySpinner.alpha = 0.0; [_mySpinner stopAnimating]; }];Errantry
C
28

Take a look at SDWebImage:

https://github.com/rs/SDWebImage

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

Tim

Crompton answered 20/3, 2012 at 13:20 Comment(2)
If someone needs resizing/cropping capabilities, I integrated this library with UIImage+Resize library. Check it out in github.com/toptierlabs/ImageCacheResizeSaccharate
this library has problem if you write info in NSTemporaryDirectory parallel to this. Any other solution?Goeselt
D
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
})
Dryasdust answered 2/12, 2014 at 10:30 Comment(2)
consider handling the failure caseCamelliacamelopard
mm confusing !! added extension func is loadFromURL, and calling to loadAsync ?Emolument
U
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
                }
            })
Urbai answered 13/12, 2017 at 6:59 Comment(0)
S
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.

Salk answered 20/3, 2012 at 11:44 Comment(3)
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?Sacrificial
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.Salk
I would add that reloadData should be called from the main thread so that the views are updated almost immediately. Thanks for the tip!Jameson
G
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);
        });
    });
}
Goeselt answered 9/9, 2015 at 14:16 Comment(0)
T
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.

Thermonuclear answered 5/9, 2013 at 15:43 Comment(0)
P
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.

Pedo answered 16/1, 2016 at 13:25 Comment(0)
A
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.

Authorize answered 20/3, 2012 at 11:40 Comment(0)
S
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)
    }
}
}
Sherrell answered 2/3, 2020 at 10:35 Comment(0)
L
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

Leshalesher answered 10/6, 2021 at 3:27 Comment(1)
this is for swiftUI only till this dateBeggs

© 2022 - 2024 — McMap. All rights reserved.