Downloading and caching images with NSURLSession/NSURLCache

Presenting images from the internet is hard

Download process is triggered by user. It has to be done asynchronously. It may be cancelled anytime. Finally for the best user experience images have to be cached. And caching is not trivial.

To lift that burden from their shoulders developers tend to reach for third party libraries like SDWebImage, Alamofire or lately Kingfisher. It’s sure tempting to forget about all the above consideration and just

imageView.setImageWithURL(imageURL)

…but is it the only sane way?

Go native!

NSURLSession comes with caching abilities of NSURLCache. It will efficiently cache HTTP responses (accordingly to their Cache-Control headers) including those with images or any other media! Satisfaction guaranteed by Apple. No need to reinvent the wheel.

In fact, one can make use of NSURLCache even when responses lack Cache-Control headers. Missing headers may be injected in response in NSURLSessionDelegate method

func URLSession(session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: (CachedURLResponse?) -> Void)

Implementation

(March, 2018) Updated code samples and the demo project to use Swift 4.

Here’s a gist with:

  • SessionDelegate - implementation of NSURLSessionDelegate able to inject Cache-Control headers
  • Synchronizer - NSURLSession client that internally uses SessionDelegate to control caching

Synchronizer ia able to load resources represented with Resource protocol. Resource requirementes are very simple: provide NSURLRequest and interpret the outcome:

protocol Resource {
func request() -> URLRequest

associatedtype ParsedObject
var parse: (Data) throws -> ParsedObject { get }
}

Synchronizer defines it’s result type…

enum SynchronizerResult<Result> {
case Success(Result)
case NoData
case Error(ErrorType)
}

…and provides a loading method:

typealias CancelLoading = () -> Void

func loadResource<R: Resource, Object where R.ParsedObject == Object>
(resource: R, completion: SynchronizerResult<Object> -> ()) -> CancelLoading

In order to support images ImageResource is introduced. As simple as that:

struct ImageResource {
let imageURL: URL
}
extension ImageResource: Resource {
func request() -> NSURLRequest {
return URLRequest(URL: imageURL)
}
var parse: (Data) throws -> UIImage? {
return { data in
UIImage(data: data)
}
}
}

Usage

let MB = 1024 * 1024
let day: TimeInterval = 24 * 60 * 60

// create caching image synchronizer
let imageCache = URLCache(memoryCapacity: 100 * MB, diskCapacity: 100 * MB, diskPath: "images")
let imageSynchronizer = Synchronizer(cacheTime: day, URLCache: imageCache)

// load image into image view
let cancelationBlock = imageSynchronizer.loadResource(ImageResource(URL: imageURL)) { (object) in
if case .Success(let image) = object {
imageView.image = image
}
}

// cancel loading when image view goes off screen
cancelationBlock()

So where is my setImageWithURL method?

Well, there is none yet. If you ain’t scared of singletons you can easily come up with an extension on UIImageView that uses shared instance of image synchronizer. You could also take care of cancelling there when imageView goes off screen or internet connection is bad.

Conslusion

The point is you cut loose a third party dependency. The solution presented here may be used next to or combined with your current networking layer. You poses full control over it. You are in charge now.

Demo

See it working in the project (Swift 4): code

image