5

I have images in my collectionViewCell's that are fetched and parsed via NSURLRequest, how do I cache these images so they don't have to start a new request with every single appearance/disappearance of the view?

here is my code that fetches the images:

class funnyPicture: NSObject {

    var pfPicture : PFObject
    var coverImage : UIImage!

    init(pfPicture: PFObject) {
        self.pfPicture = pfPicture
    }

    func fetchCoverImage(completion: (image: UIImage?, error: NSError?) -> Void) {
        let urlString = self.pfPicture["funnyPictures"] as! String
        let url = NSURL(string: urlString)
        let request = NSURLRequest(URL: url!)
        let queue = dispatch_get_main_queue()

        NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) { (response: NSURLResponse?, data: NSData?, error: NSError?) in
            if error == nil {
                self.coverImage = UIImage(data: data!)
                completion(image: self.coverImage, error: nil)
            } else {
                completion(image: nil, error: error)
            }
        }
    }
}

and here is my collectionView code that parse the images to the collectionViewCell's:

   override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! MyCollectionViewCell

        // Configure the cell
        let book = self.books[indexPath.row]
        let coverImage = book.coverImage
        if coverImage == nil {
            book.fetchCoverImage({ (image, error) -> Void in
                if self.collectionView != nil {
                    collectionView.reloadItemsAtIndexPaths([indexPath])
                }
            })
        } else {
            dispatch_async(dispatch_get_main_queue()){
                let imageView = cell.imageView
                imageView.image = book.coverImage
            }
        };
        if book.coverImage == nil {
            cell.imageView.userInteractionEnabled = false
            cell.userInteractionEnabled = false
        }else {
            cell.imageView.userInteractionEnabled = true
            cell.userInteractionEnabled = true

        }

        return cell
    }

While I've received references to third party frameworks, I haven't received any answer on how to implement them with the code I have provided in the question, or even an answer using apples already implemented caching mechanism.. The reason I put the code in the question was for use in an answer.. Thank you.

Mike Strong
  • 940
  • 3
  • 13
  • 31

6 Answers6

4

Here is an example for your collection view cell:

import UIKit

let imageCache = NSCache<AnyObject, AnyObject>.sharedInstance

class myCell: UICollectionViewCell {

    @IBOutlet public weak var myImageView: UIImageView?

    private var imageUrlString: String?

    private var downloadTask: URLSessionDownloadTask?
    public var imageURL: URL? {
        didSet {
            self.downloadItemImageForSearchResult(imageURL: imageURL)
        }
    }

    override func awakeFromNib() {
        super.awakeFromNib()

        // Initialization code
    }

    public func downloadItemImageForSearchResult(imageURL: URL?) {

        if let urlOfImage = imageURL {
            if let cachedImage = imageCache.object(forKey: urlOfImage.absoluteString as NSString){
            self.myImageView!.image = cachedImage as? UIImage
        } else {
            let session = URLSession.shared
            self.downloadTask = session.downloadTask(
                with: urlOfImage as URL, completionHandler: { [weak self] url, response, error in
                    if error == nil, let url = url, let data = NSData(contentsOf: url), let image = UIImage(data: data as Data) {

                        DispatchQueue.main.async() {

                            let imageToCache = image

                            if let strongSelf = self, let imageView = strongSelf.myImageView {

                                imageView.image = imageToCache

                                imageCache.setObject(imageToCache, forKey: urlOfImage.absoluteString as NSString , cost: 1)
                            }
                        }
                    } else {
                        //print("ERROR \(error?.localizedDescription)")
                    }
            })
            self.downloadTask!.resume()
        }
      }
    }

    override public func prepareForReuse() {
      self.downloadTask?.cancel()
      myImageView?.image = UIImage(named: "ImagePlaceholder")
    }

    deinit {
      self.downloadTask?.cancel()
      myImageView?.image = nil
    }
}

Don't forget to make an extension for NSCache Like this:

import Foundation

extension NSCache {
  class var sharedInstance: NSCache<NSString, AnyObject> {
      let cache = NSCache<NSString, AnyObject>()
      return cache
  }
}
Mina
  • 2,167
  • 2
  • 25
  • 32
Illya Krit
  • 925
  • 1
  • 8
  • 9
1

Use NSCache and NSOperationQueue to manage your image loading. There's a good post outlining the technique at https://stackoverflow.com/a/12721899/5271191 (It's Objective-C, but the technique is the same for Swift.)

Community
  • 1
  • 1
Dan Nichols
  • 469
  • 2
  • 5
  • yes i see, that method seems right but i have a small question – Mike Strong Sep 15 '15 at 03:56
  • when I add the `var imageDownloadingQueue: NSOperationQueue` and `var imageCache: NSCache` variables, It says my class has no initializers.. why so? – Mike Strong Sep 15 '15 at 03:57
  • and i have the following in my viewDidLoad `self.imageDownloadingQueue = NSOperationQueue()` `self.imageDownloadingQueue.maxConcurrentOperationCount = 4` `self.imageCache = NSCache()` ... is that not initializing? – Mike Strong Sep 15 '15 at 03:58
  • `viewDidLoad` is called after the class is initialized, so it's not the same as an `init` function and Swift will throw a compile error when you try to initialize non-optional properties there. You can set the properties during the declaration to get around this, i.e. `var imageDownloadingQueue = NSOperationQueue()`. Otherwise make them optionals and unwrap them when you go to use them. – Dan Nichols Sep 15 '15 at 06:04
1

I highly recommend you to use a clean in place replacement/extension for UIImageView, that will manage caching of the image all transparently to you and avoid unwanted complexity of maintaining operation queues, etc.

If in memory caching suffices your needs - check this out-

https://github.com/nicklockwood/AsyncImageView

If you want persistent caching, then this one will do-

https://github.com/rs/SDWebImage

HTH.

Shripada
  • 6,296
  • 1
  • 30
  • 30
  • is there not any repos or tutorials written in swift? – Mike Strong Sep 15 '15 at 05:41
  • You can include them in your swift project, and Xcode will take care of creating a bridge header, where you just need to #import the relevant headers.. – Shripada Sep 15 '15 at 05:43
  • Hey MikeStrong, take a look at my [answer](http://stackoverflow.com/questions/32577207/caching-images-in-collectionviewcell/32591457#32591457) (in regards to Swift) – kean Sep 15 '15 at 16:48
1

You should use a specialized framework for that. I would not recommend using SDWebImage, it is very outdated and is not stable.

Take a look at those two libraries that are up to date with iOS platform:

  • DFImageManager - advanced framework written in Objective-C but featuring nullability annotations (works great with Swift). Here's a list of things that make it better, than SDWebImage. Disclosure: it's written by me, opinion might be biased.
  • Kingfisher - lightweight library written in Swift. Similar to SDWebImage, but has much less features that SDWebImage and DFImageManager.
elixenide
  • 44,308
  • 16
  • 74
  • 100
kean
  • 1,561
  • 14
  • 22
  • i just need a caching mechanism, I'm not looking to change the methods I use ti request and parse the images. So is it possible to just implement an external cache ? – Mike Strong Sep 15 '15 at 18:45
  • Well, it is possible :) But this problem has been solved by multiple frameworks already (you can also throw AFNetworking, AlamofireImage, PINRemoteImage into the mix). There are a lot of things to take into consideration and those frameworks do all the heavy lifting for you. It's up to you to decide which one to use. Sorry, I would not help to reimplement this stuff. – kean Sep 15 '15 at 19:00
1

I have images in my collectionViewCell's that are fetched and parsed via NSURLRequest, how do I cache these images so they don't have to start a new request with every single appearance/disappearance of the view?

The URL loading system already provides a cache. Take a look at the docs for NSURLCache. If the resources you need aren't already being sufficiently cached, you probably only need to adjust the disk space allocated to the URL cache for your app.

You should also take a look at the headers (cache-control, expires, etc.) that come back with your resources to make sure that they're not preventing caching. Here's a short tutorial on cache-related headers.

Caleb
  • 124,013
  • 19
  • 183
  • 272
-1

I have created a library using swift 2 to do the request for image and cache it. it's very simple just give it a try.

https://github.com/georgehadly/GHImageCaching

all you can do is something like this ,

viewImg.getCachedImage("geo", URI: NSURL(string: "https://s-media-cache-ak0.pinimg.com/236x/8e/5a/98/8e5a98795dc2c5322cac97343a6cad6d.jpg")!) { (done) -> Void in
        if(done){
            // your extra
        }
    }

in case you want to delete all cached images

UIImageView.deleteAllCaching()
George Hanna
  • 354
  • 1
  • 19