iOS: How To Download Images Asynchronously (And Make Your UITableView Scroll Fast)

UPDATE: The code below doesn’t work. Please read this update first!

I’ve recently been working on an app called FoodSquare, it’s basically Yelp sorted by Foursquare checkins instead of user reviews. Just like in the Yelp app, I wanted to populate an image for each restaurant on my UITableView. It’s not done yet, but here is a screenshot:

To get the images to load, I started out by storing the image URL that was returned via the FourSquare API, and then in the tableView:cellForRowAtIndexPath: method, getting the image data through the image url like this:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
   Venue *venue = ((Venue * )self.venues[indexPath.row]);
   cell.imageView.image = [UIImage imageWithData:
                                   [NSData dataWithContentsOfURL:venue.imageURL]];
}

While this seems reasonable, when I actually loaded up my table view, I noticed that scrolling was really slow. The UIImage imageWithData: method is NOT asynchronous, so as the table view loads each new cell, it has to go out to the image url and download the data, locking up the app while doing so.

So I made it my mission this weekend to figure out how to get the images asynchronously. Luckily, my friend, an iOS developer, Nick O’Neill was in town, and gave me a few amazing pointers on how to load the images according to Apple’s recommendation.

First, Nick pointed me to the SDWebImage library, that has a lot of features for working with images, including getting them asynchronously. However, if you’re just looking for that one functionality, there is a simple way to handle image downloads by first creating this method:

- (void)downloadImageWithURL:(NSURL *)url 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 alloc] initWithData:data];
                                    completionBlock(YES,image);
                                } else{
                                    completionBlock(NO,nil);
                                }
                           }];
}

Basically, you send in a URL (or you can modify the method to send in a url string if you want) and a completion block, and do a normal asynchronous url request to the image url. You know that the data that will be returned will be an image, which you instantiate, and pass in to the passed in completion block.

Now, in my tableView:cellForRowAtIndexPath: method, I can download the image and save it asynchronously:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *cellIdentifier = @"venue";
    UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];

    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
    }

    Venue *venue = ((Venue * )self.venues[indexPath.row]);
    if (venue.userImage) {
        cell.imageView.image = venue.image;
    } else {
        // set default user image while image is being downloaded
        cell.imageView.image = [UIImage imageNamed:@"batman.png"];

        // download the image asynchronously
        [self downloadImageWithURL:venue.url completionBlock:^(BOOL succeeded, UIImage *image) {
            if (succeeded) {
                // change the image in the cell
                cell.imageView.image = image;

                // cache the image for use later (when scrolling up)
                venue.image = image;
            }
        }];
    }
}

I really love the use of blocks in the downloadImageWithURL:completionBlock: – it finally makes sense to me why block are so important in iOS development. Looking forward to using this type of pattern a lot more!

UPDATE: You can get the full project code on my Github here. Check the ViewController.m file to see the full code quoted in this post.

Enjoy the article? Join over 14,500+ Swift developers and enthusiasts who get my weekly updates.

  • maddy

    Hi Natasha,

    Can you please tell me what is Venue in this code.

    • It doesn’t matter what the Venue is for the purpose of this code. The url should be the whatever the url for the image is. In my case, I have a Venue object, which is a Foursquare location, which has an image url.

  • Patrick

    Thanks a lot for your code. You made me finally understand blocks!

  • Patrick

    Is Venue a Singleton class? Thank you!

    • Nope, it’s just a subclass of NSObject. A venue object has a name, location, address, url, etc..

  • Patrick

    I don’t understand what this line means: Venue *venue = ((Venue * )self.venues[indexPath.row]); Thanks again.

    • My tableview data source is an array called self.venues of Venue objects. I get the venue at the indexPath.row spot, and I have to typecast is at (Venue *), since an object in an array could be anything.

  • Sero Eskandaryan

    Do you have any recommandations on matching the image with the content in the cell? When i parse an xml document and parsing is finished, the link i get from the parsed xml is sent to a function to get the image urls. Up to there it works perfectly fine, the image and the link match according to the feed. But when i try to download the images and put them in the cell i get an image where its not suppose to be. Its almost as if whichever one finishes downloading first gets put as that cell’s image. Any help on that?

    • I recommend creating an object, such as the Venue one I created, that has the Image URL stored. Then using an array of these objects as your tableview data source.

  • Patrick

    Thanks a lot for your answers!

  • Patrick

    Another novice question: since you typecasted the Venue class to benefit from its object’s array in the tableView, where did you alloc/init it previously or there isn’t such a need? Thanks again for bearing with me!

  • Patrick

    Again, thanks for sharing your code on Github 🙂

  • Сергей

    Tested on iOS 7 and iPhone 4S the app crashes sometimes, but that’s not the case – the scrolling does lag significantly when loading images and is nowhere near the scrolling of Instagram’s app for example.

    • That’s because Instagram has 5 engineers working on optimizing the algorithm to load these 🙂 Especially since it’s their core business. Check out this library recently released by Path, another popular photo sharing app with amazing engineers: https://github.com/path/FastImageCache.

      • Сергей

        Wow, thanks for the great link! Was blown away by their approach!

  • John Armin Seubert

    So this works perfectly on the simulator but for some reason doesn’t on my phone (4). The pictures repeat/aren’t the right ones. If I scroll around enough, eventually they all sort themselves out. Any thoughts?

    • Are you attaching the pictures to the correct object for the table view cell? In my case, my object was a Venue, and as soon as the image for the correct venue downloaded, I added the image property to it. So when the cell is populated, it first checks that the venue has an image, and if not, downloads it.

      • Also, another thing it could be is that the cell is being re-used, so if you don’t clear out the image (set imageView to nil) and don’t have a new image to replace the existing one, it’ll show the old image. So make sure to do cell.imageView = nil when re-using the cell!

  • Andrew Robinson

    I know this is an older post, but the GCD code should be updated so that when it hits the main queue it checks to see if the cell is still on screen. Since you are reusing the cells, if scrolling fast you will see wrong images all over the place until the scrolling comes to a halt and the image loads for the cell on screen.

    • Andrew Robinson

      To clarify, “if (succeeded)” should be changed to:
      if (succeeded) {
      UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
      if (cell) {
      // change the image in the cell
      cell.imageView.image = image;
      }
      // cache the image for use later (when scrolling up)
      venue.image = image;
      }

    • Gautam Jain

      Hi Andrew. Yup. I’m facing the issue you mentioned. How should I go ahead and resolve it?

  • Hi Natasha. Great post. Thank you. Funny I’m actually doing the same exact task.. even using the FS API. Since you’re beginning to drink the “block cool-aid” as well I was wondering if you have checked out AFNetworking yet? Highly recommend. Thanks again! http://afnetworking.com/

  • Chanchal Raj

    Thanks a lot for the simple tutorial..

  • Nice. Thanks,

  • thank you! short but straightforward!

  • César

    Nice! thanks for sharing.

  • shwentn

    Hi Natasha, this is great post thank you!

    i have a little question. im using plist array to count my all local data. It works good. But i need to show many images from web on detail screen. So do you have any idea?

  • ozeca

    Muito Obrigado 🙂

  • Hayden Evans

    Great work here! Your blog has been an incredibly useful place I find myself coming back to for various issues I run into in iOS. Keep up the good work!

  • Patrick

    This was just what I neeeded! Many thanks

  • Damian

    Thanks Natasha! I just started to develop my first iOS app and decided to go straight to Swift – although I find myself frequently translating from Objective-C… I translated the above for anyone who comes across your post like me.

    https://github.com/dsieczko/downloadImageWithUrl/blob/master/downloadImageWithUrl.swift

  • dhaval

    how can i get the same functionality in Swift?

  • dhaval

    hi…I want to use the same functionality in swift…do we have any code???

  • Abhay

    hii natasha madam,
    can u help me how to store the image in the database and how can fetch this image in the database in iOS swift language.

  • Nouman Pervez

    Great solution, works perfect. Thanks Natasha..

  • Avijit

    Thank you Natasha Murashev, and Andrew Robinson it works for me

  • joefrat

    This is a fantastic solution. I like the simplicity and it avoids the less than desirable solution in Apple’s Sample “Lazy Loader” app which waits until the table stops scrolling before loading images. THANK YOU!

  • Eric Greenhouse

    isn’t there a major design issue here? [in your solution] if you set
    your cell heights to something large (like only able to display 2 cells
    at a time) and then run it, you should see images repeating or initially
    loading in the wrong index paths. this is due to the reusable cell design
    pattern: the next cell that scrolls into view at the bottom, is the same
    as the last cell scrolled out from the top (and vice versa)

    • Eric Greenhouse

      i figured out what you have to do to avoid this.

      you shouldn’t update the the image view of that cell in your callback (this is what causes the incorrect image flashing when scrolling before the images have come back).

      instead you should call reloadRowsAtIndexPaths:withRowAnimation: (or

      reloadItemsAtIndexPaths: for collectionViews). this will ensure that the
      correct index path gets updated and not simply the memory location of the
      reusable cell that is currently displayed.

      additionally, you should probably pass a reference for venue to another
      class outside of the table view or table view cell (outside of the reusable cell
      design pattern), and then cache your venue.image there.
      doing this would be more of a safety measure and will simply ensure
      that your callback will cache the return image to the correct ‘venue object’
      index in your venues array.
      (ie. i am not sure if the correct venue object is maintained when your
      callback comes back and caches the image)

      i would love some feedback or your thoughts on this. the reusable cell
      design pattern is very effective but also quite complicated and
      after 3 or 4 iOS projects of having this issue, i believe i have finally figured
      out the best way to load images async. if anyone wants to see my code
      or has questions, feel free to message me!

      • Eric Greenhouse

        hi

        @natashatherobot:disqus i was wondering if you had any feedback to my comments above. i’m very curious know if i may be missing something, or perhaps i am performing extra steps that are not actually needed. thanks!

      • MXNMike

        Will you be more specific? Im having the same issue when loading a lot of rows with 2 images per cell

        • Eric Greenhouse

          hi @mxnmike:disqus let me know if this category helps out any…..
          https://gist.github.com/greenhouse

          • MXNMike

            @Erick Greenhouse, is there any possible way to do it on a separated class instead of a category?? I’m implementing right now a class for downloading, caching and storing the images, but when I start filling up my tableview cells. It loads really weird, specially when you scroll all the way to the bottom, on the last cells, it loads the images of all the top ones and then at the end places the correct images.

          • Eric Greenhouse

            yeah….
            when is it that you are updating the image views of the cells? (ie. when download is complete or after storing or both?)

            instead of simply updating the object that holds the cell details, or updating the cell itself….
            “you should call reloadRowsAtIndexPaths:withRowAnimation: (or

            reloadItemsAtIndexPaths: for collectionViews). this will ensure that the
            correct index path gets updated and not simply the memory location of the
            reusable cell that is currently displayed.”

            try to think about how the memory management for the table view design pattern works….
            – if 3 cells are visible at time (per screen size),
            – then cell 1 will become cell 4 (as cell 1 scrolls out of view and cell 4 scrolls into view)

            you need to try to design your photo updates, etc. in a way that ensures your cell re-usable memory locations are always refreshed (or set again)

          • MXNMike

            It’s only when it’s getting downloaded, once are stored I don’t have that issue, but make sense what you are saying I was trying to look for that method but I didn’t find it, I will try a little bit later when I arrive home.

            Thanks

          • Eric Greenhouse

            sounds good, hit me up whenever, i’m always online : )

          • MXNMike

            Ok, Im having issues with where to implement this logic, im lost now… so here is the scenario, I have my mainVC, a customCell, and inside the cell i have a method called bindData where I download the image and set it to the cell….

            The question is, where should i implement the reloadRowsAtIndexPath?

            – (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
            {
            customCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
            [cell setSelectionStyle:UITableViewCellSelectionStyleNone];

            cell.logoLocation.image = defaultlogo;
            cell.imageLocation.image =defaultImage;
            MOBBusiness *info = [businessArray objectAtIndex:indexPath.row];
            [cell bindData:info];
            return cell;

            }

            customCellMethods

            -(void)bindData:(MOBBusiness*)info{

            tmpBusiness = info;

            __weak typeof(self) weakSelf = self;

            NSString *urlLogo = nil;
            NSString *tmpPath = nil;

            managerWS = [WSManager new];
            managerWS.delegate = self;

            urlLogo = logoURL;

            NSString *productIdentifier = stringforimagename];

            NSString *defaultImage = defaultimage

            [managerWS getImageWithURLString:urlLogo productIdentifier:productIdentifier defaultImage:defaultImage completionHandler:^(UIImage *image) {

            [weakSelf continueWithLogoImage:image];

            }];
            }

            – (void)continueWithLogoImage:(UIImage*)logoImage {

            UIImage *imageLogo = [[UIImage alloc]init];
            imageLogo = [logoImage copy];

            urlImage = [NSString imgURL;
            NSString *productIdentifier = stringForImageName;

            NSString *defaultImage = defaultImage;

            WSManager *manager = [WSManager new];
            manager.delegate = self;

            [manager getImageWithURLString:urlImage productIdentifier:productIdentifier defaultImage:defaultImage completionHandler:^(UIImage *image) {

            self.logoLocation.image = imageLogo;
            self.imageLocation.image = image;

            }];
            }

          • Eric Greenhouse

            sorry bro, i had a long detailed answer written out for you the other day, and now all of a sudden i don’t see it…. unfortunately i’m under a very tight deadline right now, but when thats done i can definitely help you some more…. basically you need to take all that stuff out of your table view cell class and put it in your table view controller class

            you should really look into that category i created, it makes things so much easier, you won’t have to do half the stuff you are doing, and you just call one line of code

            just #import “UIImageView+Networking.h” at the top of your table view controller class, and then inside your tableview:cellforindex delegate call it with [cell.yourimageview setImageAsyncWithUrlString….] and that should be it! everything will be cached and ready to go for you!

            good luck! feel free to hit me up in a couple days or whenever if i forget to come back and give you a more detailed response, sorry ; ) greenhouse.eric@gmail.com

  • Göktuğ Yılmaz

    There is a method in here that downloads the image: https://github.com/goktugyil/EZSwiftExtensions

    Its: ez.requestImage()

  • gngrwzrd

    Please checkout UIImageLoader. I put this together for this exact situation. It gives you callbacks for the exact situations you need to handle. And there are two great samples in the repo. https://github.com/gngrwzrd/UIImageLoader

  • Mandic Milos

    Hello, do you have this article for Swift 2.0?

  • Prabh Dhaliwal

    Hey… I am facing problems in Swift as dequeueReusableCellWithIdentifier will always return a cell something like below code because of which it’s not updating images on scrolling tableview. Any idea how we can fix this?

    let cell = tableView.dequeueReusableCellWithIdentifier(“CustomCellIdentifer”, forIndexPath: indexPath) as! CustomCellIClass