Created
September 4, 2014 15:25
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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