Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.