Developing iOS Apps Using Swift Part 5 – Async image loading and caching

This section completely updated to reflect changes in Xcode 6.3, as of April 16, 2015

In parts 1 through 4 we went over some basics of Swift, and set up a simple example project that creates a Table View and a puts some API results from iTunes inside of them. If you haven’t read that yet, check out Part 1

This table is slow! Let’s speed it up.

So, we now have the functionality we’re looking for, but if you run this yourself you’ll see its super slow! The issue is that the images in these cells are downloading on the UI thread, one at a time, and they aren’t being cached at all. So let’s fix that.

Let’s start by adding a lookup dictionary as a member for our SearchResultsViewController class:

var imageCache = [String:UIImage]()
Dictionary syntax
This is the first time we’ve seen this syntax so let me explain real quick.
The type specified here is [String : UIImage], this is similar to the Objective-C NSDictionary type, a HashMap in Java, or just Hash in Ruby/JS, but in Swift it’s very strict about the type. It takes a String as a key, and stores a UIImage as a value; no other types will be accepted.

So for example, if I have an image named “Bob” set to the UIImage with the file name “BobsPicture.jpg”, I might add him to the dictionary like this:

imageCache["Bob"] = UIImage(named: "BobsPicture.jpg")

The UIImage(named: “Bob.jpg”) part is just to get a UIImage from the file named Bob.jpg, this is standard Swift syntax for local files, the dictionary uses a subscript to set or retrieve it’s values. So if I wanted to get that image of Bob back out, I could just use:

let imageOfBob = imageCache["Bob"]

The reason we add the two parentheses is to call the constructor to init the empty dictionary.
Just like how we use APIController(), we need to use [String : UIImage]().

Hope that makes sense, if not yell at me on Twitter about it.

Now, in our cellForRowAtIndexPath method, we want to do quite a few things, we basically want to access the images in our cache, or download them if they don’t exist while on a background thread. After a new image is downloaded, it should be stored in to the cache so we can access it the next time the image is needed, without needing to go start another download process.

Let’s start by moving the imgData call out of our optional chain, and instead put it inside of the block that updates the cells. We’ll do this so that we can use image data from our cache *or* perform a new download, depending on the situation. Here, we’re switching to using NSURLConnection’s sendAsynchronousRequest method in order to download the image data on a background thread.

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier) as! UITableViewCell
    if let rowData: NSDictionary = self.tableData[indexPath.row] as? NSDictionary,
        // Grab the artworkUrl60 key to get an image URL for the app's thumbnail
        urlString = rowData["artworkUrl60"] as? String,
        imgURL = NSURL(string: urlString),
        // Get the formatted price string for display in the subtitle
        formattedPrice = rowData["formattedPrice"] as? String,
        // Get the track name
        trackName = rowData["trackName"] as? String {
            // Get the formatted price string for display in the subtitle
            cell.detailTextLabel?.text = formattedPrice
            // Update the textLabel text to use the trackName from the API
            cell.textLabel?.text = trackName
            // Start by setting the cell's image to a static file
            // Without this, we will end up without an image view!
            cell.imageView?.image = UIImage(named: "Blank52")
            // If this image is already cached, don't re-download
            if let img = imageCache[urlString] {
                cell.imageView?.image = img
            else {
                // The image isn't cached, download the img data
                // We should perform this in a background thread
                let request: NSURLRequest = NSURLRequest(URL: imgURL)
                let mainQueue = NSOperationQueue.mainQueue()
                NSURLConnection.sendAsynchronousRequest(request, queue: mainQueue, completionHandler: { (response, data, error) -> Void in
                    if error == nil {
                        // Convert the downloaded data in to a UIImage object
                        let image = UIImage(data: data)
                        // Store the image in to our cache
                        self.imageCache[urlString] = image
                        // Update the cell
                        dispatch_async(dispatch_get_main_queue(), {
                            if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
                                cellToUpdate.imageView?.image = image
                    else {
                        println("Error: \(error.localizedDescription)")
    return cell

So what’s all this code mean? Let’s run through the changes real quick…


Before we download the real image we set the cell’s placeholder image. This is required if you want the cell to actually include an image view. Otherwise even loading in our image later will not show up! Create a blank image (I’m using 52×52 pixels, but it doesnt matter much) and Import it in to your project by click+dragging a file from finder in to you Xcode project, name it Blank52, and then set our cell to use this image. You can just grab my image file here (right-click and save as…)

cell.imageView?.image = UIImage(named: "Blank52")

Now our app should be less prone to crashing, and will now always have an image cell.

Put Image Downloading On A Background Thread

Now we need to check our image cache to see if this image has been downloaded before. We use the optional binding to check for the existence of our image in the cache:

if let img = imageCache[urlString] {
    cell.imageView?.image = img

If the image doesn’t exist (and initially it won’t) we need to download it. There are a couple of ways to initiate a download. Previously we used NSData’s dataWithContentsOfFile, but here we’re going to switch to NSURLConnection’s sendAsynchronousRequest, more similar to how our API works. The reason being is that we want to send off lots of small requests for images real quick, and we want to do it in the background. So let’s do that.

Look at the line with a call to NSURLConnection’s static method sendAsynchronousRequest, which takes a function/closure as a parameter for completionHandler. The lines after this call represent a function that is executed only *after* the async request returns.

NSURLConnection.sendAsynchronousRequest(request, queue: mainQueue, completionHandler: { (response, data, error) -> Void in
    if error == nil {
        // Convert the downloaded data in to a UIImage object
        let image = UIImage(data: data)
        // Store the image in to our cache
        self.imageCache[urlString] = image
        // Update the cell
        dispatch_async(dispatch_get_main_queue(), {
            if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
                cellToUpdate.imageView?.image = image
    else {
        println("Error: \(error.localizedDescription)")

Inside the block we will get back a few variables: response, data, and error.

If an error exists, proceed to create a UIImage from the data using the UIImage(data: data) constructor.

Next, we set the image cache to save our fancy new image with a key of the image URL. Using the URL as the key means we can find the image in our dictionary any time it pops up, even in a completely different context.

self.imageCache[urlString] = image

Finally, we set the cell image, on the UI thread (main queue):

dispatch_async(dispatch_get_main_queue(), {
    if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
        cellToUpdate.imageView?.image = image

You’ll notice we’re also using the cellForRowAtIndexPath() method here. The reason we use this is because sometime’s the cell that this code was running for may no longer be visible, and will have been re-used. So, to avoid setting an image on an unintended cell, we retrieve the cell from the tableView, based on the index path. If this comes back nil, then we know the cell is no longer visible and can skip the update.

Okay! Give the project a run and see our amazing new blazingly fast, silky smooth table view!

The complete code up to this point is available on Github as a branch ‘Part5‘.

To get updates on this tutorial in your email, subscribe here. I’m also working on a book filled with practical tips and tutorials on working with Swift to develop iOS 8 apps. It’s now available for pre-order here.

Part 6 will focus on adding a new View Controller that we can open up to, loading it with iTunes data.

Go to part 6 now ->

This Post Has 70 Comments

  1. Hey Jameson,

    you got some pretty neat tutorials over there. They are really helpful to understand the basics of swift 🙂 Thank you very much!

    Also i would like to add a short comment for everybody who is wondering why their table-cells don’t display any images until you click on them.
    It seems like you need to initialize the cell-image before you call the dispatch-block. Otherwise the image won’t be refreshed at the “cell.image = image”-part. You can find the code in the Part 5 – SearchResultsViewController.swift but its not written in the tutorial itself.

    Best regards from Germany

    1. Thanks for this, I will edit the tutorial so this is included.

      1. Sorry – Jameson – can you point to the code which initializes cell-image?

        1. let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier) as UITableViewCell

    2. Trying this tutorial in Jun 2015…

      I forgot to add the image to my project and got the same results: the image didn’t appear until I clicked on a row. Once I added the image to my project, all was well.

  2. Is there a better way to do this? Seems a bit buggy when scrolling,

    I load it, i see the icons,, i start scrolling and they disappear and don’t come back

    1. Hm, I’m not seeing the issue on my build. Try cloning my github repo and see if it does it there.

      I’m going to guess your cache is not working, since that’s where it would pull the image from on the second load.

      1. I figured out the issue when it comes to icons not showing up again. The else case was omitted in the “if !image?” if/else statement in the cellForRowAtIndexPath git code you provided. I’m still encountering some bugs in the fact that when I scroll past some icons and scroll back up they revert back to the blank icon image before loading the actual icons again much later.

        1. Again, this is a case of not using the cache, if the cache is being used images will instantly show up.

          1. Hmm, well at this point my code is identical to yours and it still isn’t working. Any idea why the cache wouldn’t be working? It’s setup exactly the same as your git code.

    2. Seems I fixed the disappearing thumbnail glitch. Here’s the whole cellForRowAtIndexPath function:

      I’m not quite sure why this was happening though. Maybe async requests firing every time a cell needs be redrawn, before the previous request finishes, and stores the image to the cache?

      There original code was also setting the image to blank first unconditionally, IIRC, and only then tried fetching.

  3. Figured it out. I inserted these lines just before the dispatch call:

    var image: UIImage? = self.imageCache.valueForKey(rowData[“artworkUrl60”] as? String) as? UIImage
    if !image? {
    cell.image = UIImage(named: “Blank52”)
    else {
    cell.image = image

    Basically just a duplicate of what was found in the dispatch call itself.
    Thanks again, can’t wait for more of this tutorial!

  4. Thanks very much for this series of tutorials – very helpful when getting started and I don’t know how you worked out so much so fast. The NSURLConnection.sendAsynchronousRequest syntax would never have occurred to me and I don’t understand the “-> Void in” at the end, but presumably I will learn.

    I have one suggestion and one request:

    You are using “var” to init all your variables and I think most of them would be better initialised using “let”. We are used to setting mutability by the choice of class, but now it seems that var & let are what define mutability. Using “let” wherever possible seems like an option that would give safer & better performing code.

    My request is for you to show us how to use Array & Dictionary instead if NSArray & NSDictionary.

    1. Sarah, it’s funny you say that, because I just now swapped out a lot of the Obj-C types with the native Swift types in my code base. Big post coming soon with all that. Also I agree on the underuse of let, that’s something I need to get used to.

  5. Thanks for your Swift tutorials, it’s a good starting point for learning the new Apple programming language! I use this code for cellForRowAtIndexPath. Only if the image not exists in cache, the app uses the dispatch_async block. This way the app works very quickly.

    func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {

    let kCellIdentifier: String = “SearchResultCell”

    var cell : UITableViewCell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier) as UITableViewCell

    if cell == nil {
    cell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: kCellIdentifier)

    var rowData: NSDictionary = self.tableData[indexPath.row] as NSDictionary

    // Add a check to make sure this exists and set default image
    let cellText: String? = rowData[“trackName”] as? String
    cell.text = cellText
    cell.image = UIImage(named: “Blank52”)

    // Get the formatted price string for display in the subtitle
    var formattedPrice: NSString = rowData[“formattedPrice”] as NSString

    // Grab the artworkUrl60 key to get an image URL for the app’s thumbnail
    var urlString: NSString = rowData[“artworkUrl60”] as NSString

    // Check our image cache for the existing key. This is just a dictionary of UIImages
    var image: UIImage? = self.imageCache.valueForKey(rowData[urlString] as? String) as? UIImage

    if !image? {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
    // Jump in to a background thread to get the image for this item
    // If the image does not exist, we need to download it
    var imgURL: NSURL = NSURL(string: urlString)
    // Download an NSData representation of the image at the URL
    var request: NSURLRequest = NSURLRequest(URL: imgURL)
    var urlConnection: NSURLConnection = NSURLConnection(request: request, delegate: self)
    NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: {(response:NSURLResponse!,data: NSData!,error: NSError!) -> Void in
    if !error? {
    image = UIImage(data: data)
    // Store the image in to our cache
    self.imageCache.setValue(image, forKey: urlString)
    cell.image = image
    } else {
    println(“Error: \(error.localizedDescription)”)
    else {
    cell.image = image
    cell.detailTextLabel.text = formattedPrice
    return cell

  6. Awesome !

  7. Would you mind telling me how to pass the data to another viewcontroller?

  8. The code did indeed speed up my table quite a bit. I am seeing a warning in the console however. Just wondering if anyone else sees the same:

    SetAppThreadPriority: setpriority failed with error 45

    The thread priority appears to be set: DISPATCH_QUEUE_PRIORITY_DEFAULT
    I commented out the NSURLConnection asynchronous request, and the message still appears, so it appears to be generated by this call.

    1. I’m on the same page, nothing displays and I get that error.

  9. Thank you for the series, but how does one force the cell to redraw at the end of the async call? I see the call being made, but the cell does not show the image until a tap is made.

    1. Make sure you set a temporary image to the cell, something loaded from disk. I just used a blank png file. If you don’t do this the cell assumes there is no image and so it hides the image view. When you load in your image later, the image view is still hidden. Only a click will force it to redraw and show up.

  10. In cellForRowAtIndexPath, what does this, tableView.indexPathForCell(cell), do?

    I commented it out of the code and the application continues to work correctly.

    Also why was NSURLConnectionDataDelegate not added as a protocol to the APIController class? Again, the application works the same when I add it in. All the methods in that protocol are optional so it does not seem to matter.
    class APIController: NSObject, NSURLConnectionDataDelegate {


    1. The cellForRowAtIndexPath() call is needed in the future, but not here. It’s a mistake that it’s included.
      You could add NSURLConnectionDataDelegate simply to indicate that it will be receiving these callbacks. That’s probably the cleaner way to go.

  11. Everything’s working great with one exception. Now with this cache and async technique, i can’t seem to get my images to show up until I either touch a row, or change the orientation of the device. When I touch one, the image shows up, and when I rotate the device, they all show up. Yet, there’s nothing in my tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) that would seem to cause such, as its just creating an alert…


    1. Make sure to set an image initially, just like a Blank.png file. If you don’t set something initially the Image View isn’t created, and so when the image loads in nothing shows, until it’s reloaded with a tap.

  12. Hi James,

    The tutorials have been a big help.

    Quick question:
    Why make the async call to fetch the images inside a background thread? sendAsyncRequest uses the callback so as to not lock up the UI.

    But maybe I’m missing something.


    1. Jeremy, you’re absolutely right. If we write it all inline the API method will end up becoming asynchronous anyway. with the callback. Removing the dispatch_async line produces the same effect.

  13. This code’s concurrency looks a bit suspect…

    (1) Imagine if tableView is called for one row, and it kicks off a background thread “A” in response by using dispatch_async

    (2) Now tableViewAsync is called a second time for a different row, and it kicks off a background thread “B”

    (3) It so happens, for argument’s sake, that both rows had the same URI for their image. Thread “A” will discover that the image is absent from the cache and so will kick off downloading of the image.

    (4) Then, thread “B” will also discover that the image is absent from the cache and so will wastefully kick off a second download of exactly the same image. So the first problem I see is that the code is wasteful.

    (5) What happens is that multiple concurrent threads will end up reading and writing to the NSMutableDictionary. However, the NSMutableDictionary isn’t thread safe, as per this link:

    Here also is an Apple technical note on the subject:

    I don’t know what the behavior of NSMutableDictionary is when two competing threads attempt to write to it. Will data be lost? (which is wasteful but not a grevious problem). Or will it throw an exception or crash or something like that?

    1. Great comments Lucian. You’re right there is a possibility users will be downloading the icons more than once in certain conditions. The likelihood that it would happen could be reduced to effectively zero if the dictionary for image contents were separated out, so that one Swift dictionary indicates that a image will be downloaded when a cell is loading in, and that happens instantly, as opposed to waiting for the image to download. The storage (after downloading) to the NSMutableDictionary could also first join the main thread again. My goal here is not to write production code though, it’s just to explain the concepts 🙂

      1. How about you change the code to completely remove the call to dispatch_async. It’s just not needed here. The only work you’re doing inside the “-> UITableViewCell!” callback is INITIATING an asynchronous request, plus a few tiny lines of code which are vanishingly cheap and are fine to perform on the UI thread.

        If you were doing long-running blocking operations then I’d understand the wish to call dispatch_async. But there aren’t any!

        1. The long running operation is the download of the image from the web.

          1. Sure it’s a long-running operation. But it’s not a long-running BLOCKING operation. No thread is blocked. All you do is *initiate* the request and then return. Hence: no need to use background thread.

  14. I’ve solved the lost images after scrolling by changing the dispatch_async to dispatch_sync in the “cellForRowAtIndexPath” func, and there is not lagging and no images ever disappears

  15. Hi Jameson,
    thanks for the awesome tutorial. It was very helpful!

    Everything works as expected but one thing keeps bothering me: In unselected cells, the app icons are shown as small as 16×16. As soon as i select cell, the icon gets blown up to the height of the cell itself. Why?

    1. I think there is probably a missing return to the foreground thread where you should refresh the tableview. I think it’s actually an issue in the tutorial, let me see if I can track it down today and fix it.

  16. Hi, I think these tutorials are great for using swift in iOS. I just had one thing that was kinda confusing to me. In SearchResultsViewController.swift, we have three ‘func tableView’ declarations. Im not exactly sure how that works, are they overwriting each other? Do they need to the same name to work? Any info on this will help.


    1. Charles, what you’re seeing here is actually method overloading. There are three methods who begin with func tableView(…

      But, none of them have the same parameters, which in Swift is part of the unique method signature.

      That is to say:

      func myFunc(myNum: Int)

      is a different function than:

      func myFunc(myNum: Float)

      Because the types of the arguments don’t match.

      1. That clears it up perfectly! Thank you so much!

  17. Hi Jameson

    In this part, in the line 32 you define this statement:
    let urlConnection: NSURLConnection = NSURLConnection(request: request, delegate: self)

    Which is the purpose of the urlConnection constant?

    I found differences between the code displayed here and the code deployed in the repository. After you store the image in the cache, you assign the image to the ImageView in the cell. Which is the difference between use or not use the dispatch_async sentence even if exists or not in the cache when you do this:

    cell.imageView.image = image



    1. Isreal, the urlConnection actually doesn’t do anything, my mistake. It’s a remnant from a refactor earlier. I’ve removed it from the repo.
      Adding the dispatch_async call moves to the foreground thread, so the update happens a little quicker. It’s not strictly necessary, but it’ll make things faster.

      1. Ok .. thanks

    1. Actually removing the GCD dispatch all together seems to be the proper solution, because the NSURLSession is already moving things to the background thread.

  18. Jameson, thanks for the tutorial! This helped solve and understand a long standing async trouble I could never wrap my head around.

    There just seems to be some discrepancies in the code when you initially show the func for cellForRowAtIndexPath, and then where you start to break it down and explain each part. I noticed that in your code sample on Github it doesn’t seem to match all of the tutorial here.

    Ex: error == nil vs !error? and
    if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
    cellToUpdate.imageView.image = image
    dispatch_async(dispatch_get_main_queue(), {
    cell.imageView.image = image

    1. I’ve updated this so many times I wouldn’t be too surprised to find some discrepancies. Let me look in to it and see if I can find the mismatched code.

  19. Hi, and thanks for the excellent tutorial!

    I was having trouble compiling this part in xCode. The trouble was in this line:
    var imageCache = [String : UIImage]()

    In Apple’s Swift handbook I found out that the syntax for this part should be the following:
    var imageCache = Dictionary ()

    That solved all the compilation errors and the project ran sweetly. That was xCode Version 6.0 (6A215l) I was using.

    1. Thanks for commenting Igor, the version you are using is actually beta 1. You’re now 5 versions behind!!!

  20. Opps, the correct code was stripped when I posted here. Try again with words))

    var imageCache = Dictionary AngleBracketOpensString, UIImageAngleBracketCloses()

  21. I got lots of stuff from your tutorial
    Thanks very much.

    But in this page, I found the dictionary declaration is not ok for my Xcode,
    It should be like this:
    var imageCache = [String(): UIImage()]

    I don’t know the reason.


  22. I just wanted to say thank you for the tutorial on async image loading. it saved lots of my time 🙂

  23. Brilliant tutorials, helped me greatly thank you!!

    One thing, the images aren’t loaded until I scroll or click the table row. I’ve tried your direct download from github and it has the same issue? any ideas?

    Also, when I open your code or copy I have to delete “?” or “!” in some cases as promoted by the Xcode IDE?

    Once thanks for such brilliant tutorials.

    1. When the API changes due to optional conformance checks by Apple, the ? or ! will sometimes change.
      The images aren’t loading until you scroll/click because you don’t have a placeholder. Without a placeholder image the image view isn’t created. It’s an optimization on Apple’s part, and sometimes it causes bugs like this ^_^

  24. You need to actually have an image named “Blank52” in your project for this to work. Otherwise the placeholder is not initialized properly

    1. As Seb wrote, without an image in the project I couldn’t have it worked.

      Moreover it must be a real picture file.
      I mean, at first I just created a blank file and renamed it Blank52.png and it was a bad idea.
      The thumbnails were loaded only when clicking and they were disappearing when scrolling. I was becoming nuts when I read a second time Seb comments and thought I should focus on this image file creation.

      So take care to add a REAL png file (whatever is inside) and copy it at the same level of any class file (there may be a better place and would be happy to learn it from someone but at least it worked for me).

      Thanks to Seb for the comment and to Jameson for the work done.

  25. Hi,

    Great tutorial.

    Can a similar setup be used for async fetching of thumbnail images from Core Data?

    I have a collection view which is pulling around a 100 small images stored in Core Data (100×100 pixels) but the view is slow and stutters when scrolling.

    I’m a bit lost on how to make this smoother.


    1. Yeah I would use pretty much the same approach for that if I were you.

  26. Hi JAMESON,
    This tutorial very useful us.

  27. Hey Jameson,

    In Xcode 6.1.1, you would need to change one line of code, otherwise you’d get a compile error.

    Line 28:
    var imgURL: NSURL = NSURL(string: urlString)

    Needs to be changed to:
    var imgURL: NSURL! = NSURL(string: urlString)

    1. Further to this, you can also wrap it in an if statement.

      if( image == nil ) {
      // If the image does not exist, we need to download it
      if var imgURL: NSURL = NSURL(string: urlString) {

      // Download an NSData representation of the image at the URL

      println(“Error: \(error.localizedDescription)”)
      } // End of the if var wrap

  28. I’m trying to use this code for a UIImageView and it keeps telling me that its finding nil for the UIImageView.

    it was created on the storyboard and control dragged to the file.

    @IBOutlet weak var ImageView: UIImageView!

    cachedImage = UIImage(data: data)
    self.imageCache[item[“image”]!] = cachedImage
    dispatch_async(dispatch_get_main_queue(), {
    self.ImageView.image = cachedImage

    fatal error: unexpectedly found nil while unwrapping an Optional value

    the string and image are there, I’ve moved stuff around to make sure and its definitely the ImageView is nil

    1. The way you’re accessing the imageCache isn’t quite right. Check the github repo to compare you code and mine.

  29. What is the purpose of using dispatch_async(dispatch_get_main_queue()?

    1. It makes sure the code you execute occurs on the main thread. Without this, your thread may or may not update the UI (eventually). On the main thread, it happens immediately.

  30. This saved me on performance, until I get down to about 150 items in my list (of 224 items). Then the application basically freezes up. Should I be releasing the images in the image cache?

    Also, I am using a custom UITableViewCell class with an ImageView in it, so I am populating the image in the cell with the downloaded image. This only works *after* I scroll past the image, however. When I first open the app, the images are blank (on first load, they get saved and then re-referenced later) until I scroll the images away, then when I scroll back up they appear. Any suggestions on why this may be happening?

    1. You need to specify a placeholder image, otherwise the imageView isn’t actually loaded until the second time around.

  31. Beautiful! Thank you for this! My app will have a small set of images that may need to expand over time without an app update, so instead of storing them in an in-memory image cache, I’m storing the images in the app document directory so each image just needs to be downloaded once.

Leave a Reply

Close Menu