Skip to content

Instantly share code, notes, and snippets.

@kwylez
Created September 4, 2014 15:25
Show Gist options
  • Save kwylez/5aae6a8660ab5491910d to your computer and use it in GitHub Desktop.
Save kwylez/5aae6a8660ab5491910d to your computer and use it in GitHub Desktop.
First attempt of using Swift. This is a port of an image cache utility that I wrote for one of my apps
//
// MSImageCache.swift
// Stasis
//
// Created by Cory D. Wiles on 8/17/14.
// Copyright (c) 2014 Macspots. All rights reserved.
//
import Foundation
import UIKit
enum MSImageCacheType: Int {
case MSImageCacheTypeNone
case MSImageCacheTypeDisk
case MSImageCacheTypeMemory
}
// move to struct
// including the queue creation
let MSDiskOperationQueueName:String = "com.macspots.fetchRequests.queue"
let MSDefaultImageCacheDirectory:String = "Macspots-Stasis-Images"
let MSSaveImageErrorDomain:String = "com.macspots.image.save.error"
let MSImageCacheRequestQueue:String = "com.macspots.image.request.queue"
typealias MSImageQueryCompletionBlock = (image: UIImage?, cacheType:MSImageCacheType!) -> Void
typealias MSImageSavingCompletionBlock = (success: Bool!, error: NSError?) -> Void
class MSImageCache: NSObject {
// MARK: Public Properties
var maxCacheSize: Int = 30 * 1024
/**
* You can't use enums so I'll have to use readonly constants
*/
let MSImageCacheTypeNone: String = "MSImageCacheTypeNone"
let MSImageCacheTypeDisk: String = "MSImageCacheTypeDisk"
let MSImageCacheTypeMemory: String = "MSImageCacheTypeMemory"
// MARK: Private Properties
private let memCache: NSCache = NSCache()
private let fileManager: NSFileManager = NSFileManager()
private let defaultImageRequestQueue: NSOperationQueue = NSOperationQueue()
private var diskOperationDispatchQueue: dispatch_queue_t = 0
private var cachePathName: String = ""
private lazy var imageRequestOperations: Dictionary<String, NSOperation> = Dictionary<String, NSOperation>()
// MARK: Class setup
override class func load() {
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: "clearMemoryCache",
name: UIApplicationDidReceiveMemoryWarningNotification, object: nil)
notificationCenter.addObserver(self, selector: "clearDisk",
name: UIApplicationWillTerminateNotification, object: nil)
notificationCenter.addObserver(self, selector: "clearDiskCacheWithBackgroundTask",
name: UIApplicationDidEnterBackgroundNotification, object: nil)
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
struct Static {
static var token : dispatch_once_t = 0
static var instance : MSImageCache?
}
class var sharedInstance: MSImageCache {
dispatch_once(&Static.token) {
Static.instance = MSImageCache()
}
return Static.instance!
}
init(imageCachePathName: String) {
super.init();
self.cachePathName = imageCachePathName;
self.diskOperationDispatchQueue = dispatch_queue_create(MSDiskOperationQueueName, DISPATCH_QUEUE_CONCURRENT);
self.memCache.name = imageCachePathName;
self.defaultImageRequestQueue.name = MSImageCacheRequestQueue
self.defaultImageRequestQueue.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount
}
convenience override init() {
self.init(imageCachePathName: MSDefaultImageCacheDirectory);
}
// MARK: Public Methods
func storeImage(image: UIImage!, cacheKey: String!, completionBlock:((success: Bool!, error: NSError?) -> Void)?) -> Void{
/**
* UIImage / NSString don't "convert" bool for checking. Should this be done?
* see https://medium.com/arthurs-coding-tips/optionals-in-swift-c94fd231e7a4
*/
let imageCost:Int = Int(image.size.height * image.size.width * image.scale)
self.memCache.setObject(image, forKey: cacheKey, cost: imageCost)
/**
* @note Need to make this queue a gloabl struct?
*/
dispatch_async(self.diskOperationDispatchQueue, {
var successfulProcessing: Bool = true
if let data:NSData = UIImagePNGRepresentation(image) {
let fileDoesExist: Bool = self.fileManager.fileExistsAtPath(self.diskCachePath())
if (fileDoesExist) {
var createCacheDirectoryError: NSError?
var saveImageError: NSError?
self.fileManager.createDirectoryAtPath(self.diskCachePath(), withIntermediateDirectories: true, attributes: nil, error: &createCacheDirectoryError)
let filePath:String = self.cachePathForKey(cacheKey, path: self.diskCachePath())
let saveImageFile:Bool = self.fileManager.createFileAtPath(filePath, contents: data, attributes: nil)
if ((completionBlock) != nil) {
if ((createCacheDirectoryError) != nil) {
successfulProcessing = false
saveImageError = createCacheDirectoryError
} else if (!saveImageFile) {
let userInfo: [String: String] = [NSLocalizedDescriptionKey : NSLocalizedString("Operation was unsuccessful.", comment: "NSLocalizedDescriptionKey"),
NSLocalizedFailureReasonErrorKey : NSLocalizedString("The save image operation failed.", comment: "NSLocalizedFailureReasonErrorKey"),
NSLocalizedRecoverySuggestionErrorKey : NSLocalizedString("Does the path exist?", comment: "NSLocalizedRecoverySuggestionErrorKey")]
successfulProcessing = false
saveImageError = NSError(domain: MSSaveImageErrorDomain, code: NSURLErrorCannotCreateFile, userInfo: userInfo)
}
completionBlock!(success: successfulProcessing, error: saveImageError)
}
}
}
})
}
func fetchImage(photo: Photo!, photoType:String!, completionBlock:((image: UIImage?) -> Void)?) {
var cacheKey: String = ""
/**
* Because I'm capturing the value in the block there has to be an initalization
* of the variable
*/
var imageURL: NSURL = NSURL()
if (photoType == "STSPhotoTypePreviewImage") {
cacheKey = photo.previewImageUUID()
imageURL = photo.previewImageSourceURL()
}
if (self.imageRequestOperations[cacheKey] != nil) {
self.imageRequestOperations.removeValueForKey(cacheKey)
}
self.fetchImageFromCache(cacheKey, completionBlock: {returnedImage, cacheType in
if let image: UIImage = returnedImage {
if (completionBlock != nil) {
dispatch_async(dispatch_get_main_queue(), {
completionBlock!(image: image)
})
}
} else {
/**
* Fetch remote image
*/
let remoteImageRequestOperation: NSOperation = NSBlockOperation.init(block: {
let imageData: NSData? = NSData(contentsOfURL: imageURL)
if let remoteImageData: NSData = imageData {
let imageFromData: UIImage? = UIImage(data: imageData!, scale: UIScreen.mainScreen().scale)
if let image: UIImage = imageFromData {
self.storeImage(image, cacheKey: cacheKey, completionBlock: {success, error in
NSOperationQueue.mainQueue().addOperationWithBlock({
if let finishedOperation: NSOperation = self.imageRequestOperations[cacheKey] {
self.imageRequestOperations.removeValueForKey(cacheKey)
if (completionBlock != nil) {
completionBlock!(image: image)
}
}
})
})
}
} else {
NSOperationQueue.mainQueue().addOperationWithBlock({
if let finishedOperation: NSOperation = self.imageRequestOperations[cacheKey] {
self.imageRequestOperations.removeValueForKey(cacheKey)
if (completionBlock != nil) {
completionBlock!(image: nil)
}
}
})
}
})
self.imageRequestOperations[cacheKey] = remoteImageRequestOperation
self.defaultImageRequestQueue.addOperation(remoteImageRequestOperation)
}
})
}
func removeImageForKey(cacheKey: String?, completionBlock:((success: Bool!, error: NSError?) -> Void)?) -> Void {
if let imageKey: String = cacheKey {
self.memCache.removeObjectForKey(imageKey)
if (self.isCacheRequestRunningForKey(imageKey)) {
self.imageRequestOperations.removeValueForKey(imageKey)
self.cancelImageRequestForKey(imageKey)
}
dispatch_async(self.diskOperationDispatchQueue, {
let imagePath: String = self.cachePathForKey(imageKey, path: self.diskCachePath())
var removeError: NSError?
var didRemoveItem = self.fileManager.removeItemAtPath(imagePath, error: &removeError)
if ((completionBlock) != nil) {
dispatch_async(dispatch_get_main_queue(), {
completionBlock!(success: didRemoveItem, error: removeError)
})
}
})
}
}
func cancelImageRequestForKey(cacheKey: String?) -> Void {
if let imageKey: String = cacheKey {
let requestOperation: NSOperation = self.imageRequestOperations[imageKey] as NSOperation!
requestOperation.cancel()
}
}
func cancellAllImageCacheRequest() -> Void {
self.defaultImageRequestQueue.cancelAllOperations()
}
// MARK: Private Methods
private func diskCachePath() -> String {
/**
* @see http://stackoverflow.com/questions/24696044/nsfilemanager-fileexistsatpathisdirectory-and-swift
*/
var isDir : ObjCBool = false
let imageCacheDirectory = NSFileManager.cachesDir().stringByAppendingPathComponent(self.cachePathName)
let cachePathExists: Bool = NSFileManager.defaultManager().fileExistsAtPath(imageCacheDirectory, isDirectory: &isDir)
if (cachePathExists == false) {
var createDirectoryError: NSError?
NSFileManager.defaultManager().createDirectoryAtPath(imageCacheDirectory, withIntermediateDirectories: true, attributes: nil, error: &createDirectoryError)
println("create directory error \(createDirectoryError)")
}
return imageCacheDirectory
}
private func cachePathForKey(keyPath: String, path: String) -> String {
let uuid:NSUUID = NSUUID(UUIDString: keyPath)
return path.stringByAppendingPathComponent(uuid.UUIDString)
}
private func imageFromMemoryCacheForKey(cacheKey: String) -> UIImage? {
let image: UIImage? = self.memCache.objectForKey(cacheKey) as? UIImage
return image
}
private func diskImageForKey(cacheKey: String) -> UIImage? {
var dataForKey: (String) -> NSData? = { (cacheKey: String) -> NSData? in
let imagePath: String = self.cachePathForKey(cacheKey, path: self.diskCachePath())
let data: NSData = NSData(contentsOfFile: imagePath)
return data
}
if let imageData: NSData = dataForKey(cacheKey) {
let image: UIImage = UIImage(data: imageData, scale: UIScreen.mainScreen().scale)
return image
}
return nil
}
private func isCacheRequestRunningForKey(cacheKey:String) -> Bool {
if let requestOperation: NSOperation = self.imageRequestOperations[cacheKey] {
return requestOperation.executing ? true : false
}
return false
}
private func clearDiskCacheWithCompletionBlock(completionBlock:Void -> Void?) -> Void {
dispatch_async(self.diskOperationDispatchQueue, {
self.fileManager.removeItemAtPath(self.diskCachePath(), error: nil)
if let block: Void = completionBlock() {
dispatch_async(dispatch_get_main_queue(), {
completionBlock()!
})
}
})
}
private func clearMemoryCache() -> Void {
self.memCache.removeAllObjects()
}
private func clearDiskCacheWithBackgroundTask() -> Void {
let application: UIApplication = UIApplication.sharedApplication()
let taskID = self.beginBackgroundUpdateTask()
self.clearDiskCacheWithCompletionBlock({
self.endBackgroundUpdateTask(taskID)
})
}
private func clearDisk() -> Void {
self.clearDiskCacheWithCompletionBlock({Void in Void})
}
/**
* Should this be in a private struct?
* Seems weird to have it in the class
*/
private func beginBackgroundUpdateTask() -> UIBackgroundTaskIdentifier {
return UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({})
}
private func endBackgroundUpdateTask(taskID: UIBackgroundTaskIdentifier) {
UIApplication.sharedApplication().endBackgroundTask(taskID)
}
private func fetchImageFromCache(imageKey: String!, completionBlock:((image: UIImage?, cacheType:String!) -> Void)?) -> Void {
if ((completionBlock) == nil) {
return
}
/**
* If for some reason the request operation is still running then we don't
* need to queue it again.
*/
if (self.isCacheRequestRunningForKey(imageKey!)) {
return
}
/**
* Step 1: Is the image cached in memory
*/
if let memoryCachedImage: UIImage = self.imageFromMemoryCacheForKey(imageKey) as UIImage! {
dispatch_async(dispatch_get_main_queue(), {
completionBlock!(image: memoryCachedImage, cacheType: self.MSImageCacheTypeNone)
})
return
}
/**
* Step 2: Is the image on disk
*/
let imageCacheRequestOperation: NSBlockOperation = NSBlockOperation(block: {
let diskImage: UIImage? = self.diskImageForKey(imageKey!)
if (diskImage != nil) {
let imageCost: Int = Int(diskImage!.size.height) * Int(diskImage!.size.width) * Int(diskImage!.scale)
self.memCache.setObject(diskImage!, forKey: imageKey!, cost: imageCost)
}
NSOperationQueue.mainQueue().addOperationWithBlock({
if let finishedOperation: NSOperation = self.imageRequestOperations[imageKey!] {
self.imageRequestOperations.removeValueForKey(imageKey!)
}
completionBlock!(image: diskImage, cacheType: self.MSImageCacheTypeDisk)
})
})
self.imageRequestOperations[imageKey!] = imageCacheRequestOperation
self.defaultImageRequestQueue.addOperation(imageCacheRequestOperation)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment