We recently released AlwaysHungry Brisbane app which was completely written in Swift! We used a few awesome libraries out there to pick up pace on the development. One of these libraries was HanekeSwift, which at the time of development, the only Swift Image Downloading & Caching library around! Despite the lack of competition, it is really good; with one caveat: iOS8+ only!
Given the slow adoption of iOS8, I’ve had to go back and add iOS7 support, which meant writing a brand new image caching library to be used in our app. Keep in mind AlwaysHungry’s image use is… extreme.
Research
iOS7 has a new network stack under the NSURLSession classes. NSURLSession comes with Task classes that facilitate uploading and downloading that frameworks like Alamofire is already using in the background.
NSURLCache is how everyone seems to be doing caching these days. It carries and in-memory and disk cache. An app can carry multiple caches too. I highly recommend reading this NSHipster article from @mattt. Focus on the caching policy settings.
Implementation Highlights
1. Creating a new cache (20mb memory cache, 100mb disk cache):
var URLCache = NSURLCache(memoryCapacity: 20 * 1024 * 1024, diskCapacity: 100 * 1024 * 1024, diskPath: "ImageDownloadCache")
2. NSURLSession with caching policy set to load from cache:
var config = NSURLSessionConfiguration.defaultSessionConfiguration()
// always try to load from cache
config.requestCachePolicy = NSURLRequestCachePolicy.ReturnCacheDataElseLoad
config.URLCache = URLCache
self.session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
3. Structure: we need 3 methods
- Get an image: SimpleCache.sharedInstance.getImage(url:NSURL, completion:((UIImage?, NSError?)->())?)
- Get an image from cache without initiating a download: SimpleCache.sharedInstance.getImageFromCache(url:NSURL, completion:(UIImage?, NSError?)->())
- Cancel a download: SimpleCache.sharedInstance.cancelImage(requestUrl:NSURL?)
4. getImage() method
When a request comes in for a URL, we first create a request:
let urlRequest = NSURLRequest(URL: url, cachePolicy: NSURLRequestCachePolicy.ReturnCacheDataElseLoad, timeoutInterval: 30.0)
Then we look into the cache first!
if let response = URLCache.cachedResponseForRequest(urlRequest) {
var image = UIImage(data: response.data)
dispatch_async(dispatch_get_main_queue()) {
completion?(image, nil)
return
}
}
In getImageFromCache, we just do this part.
Not there? Then we initiate a NSURLSessionDataTask and commit the successful response into the cache:
let task = self.session.dataTaskWithRequest(urlRequest) { [weak self] (data, response, error) in
...
strongSelf.URLCache.storeCachedResponse(NSCachedURLResponse(response:response, data:data, userInfo:nil, storagePolicy:NSURLCacheStoragePolicy.Allowed), forRequest: urlRequest) // commit it into the cache
...
}
task.resume()
5. cancelImage() simply removes the task from the queue.
I chose not to stop the download due to the nature of its use in AlwaysHungry.
Usage
SimpleCache.sharedInstance.getImage(request) { image, error in
if let err = error {
// thou shall handle errors
} else if let fullImage = image {
imageView.image = fullImage
}
}
override func prepareForReuse() {
...
SimpleCache.sharedInstance.cancelImage(requestUrl)
}
SimpleCache.sharedInstance.getImageFromCache(itemUrl) { image, error in
...
}
Happy days!
https://github.com/m2d2/SimpleImageCache
Performance of this cache is on par with what HanekeSwift was managing. It is extremely simple to use and understand. I am opening the source for this on Github so you can roll your own solutions with this implementation as a start point.
There’s a lot to be improved. Couple of things you may want to tackle:
- Queueing the downloads by introducing a NSOperationQueue
- Ability to cancel and resume downloads
- Better error handling
Leave a Reply