Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
aschuch committed Jul 12, 2014
0 parents commit 604b084
Show file tree
Hide file tree
Showing 19 changed files with 1,699 additions and 0 deletions.
20 changes: 20 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate

.DS_Store
11 changes: 11 additions & 0 deletions AwesomeCache.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Pod::Spec.new do |s|
s.name = "AwesomeCache"
s.version = "0.1"
s.summary = "TODO"
s.homepage = "https://github.com/aschuch/AwesomeCache"
s.license = { :type => "MIT", :file => "LICENSE" }
s.author = { "Alexander Schuch" => "[email protected]" }
s.platform = :ios, "8.0"
s.source = { :git => "https://github.com/aschuch/AwesomeCache.git", :tag => s.version.to_s }
s.source_files = "AwesomeCache/*.swift"
end
268 changes: 268 additions & 0 deletions AwesomeCache/AwesomeCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
//
// Cache.swift
// Example
//
// Created by Alexander Schuch on 12/07/14.
// Copyright (c) 2014 Alexander Schuch. All rights reserved.
//

import Foundation

/**
* Represents the expiry of a cached object
*/
enum AwesomeCacheExpiry {
case Never
case Seconds(NSTimeInterval)
case Date(NSDate)
}

/**
* A generic cache that persists objects to disk and is backed by a NSCache.
* Supports an expiry date for every cached object. Expired objects are automatically deleted upon their next access via `objectForKey:`.
* If you want to delete expired objects, call `removeAllExpiredObjects`.
*
* Subclassing notes: This class fully supports subclassing.
* The easiest way to implement a subclass is to override `objectForKey` and `setObject:forKey:expires:`, e.g. to modify values prior to reading/writing to the cache.
*/
class AwesomeCache<T: NSCoding> {
let name: String // @readonly
let directory: String // @readonly

// @private
let cache = NSCache()
let fileManager = NSFileManager()
let diskQueue: dispatch_queue_t = dispatch_queue_create("com.aschuch.cache.diskQueue", DISPATCH_QUEUE_SERIAL)


/// Initializers

/**
* Designated initializer.
*
* @param name Name of this cache
* @param directory Objects in this cache are persisted to this directory.
* If no directory is specified, a new directory is created in the system's Caches directory
*
* @return A new cache with the given name and directory
*
*/
init(name: String, directory: String?) {
// Ensure directory name
var dir: String? = directory
if !dir {
let cacheDirectory = NSSearchPathForDirectoriesInDomains(.CachesDirectory, .UserDomainMask, true)[0] as String
dir = cacheDirectory.stringByAppendingFormat("/com.aschuch.cache/%@", name)
}
self.directory = dir!

self.name = name
cache.name = name

// Create directory on disk
if !fileManager.fileExistsAtPath(self.directory) {
fileManager.createDirectoryAtPath(self.directory, withIntermediateDirectories: true, attributes: nil, error: nil)
}
}

/**
* @param name Name of this cache
*
* @return A new cache with the given name and the default cache directory
*/
convenience init(name: String) {
self.init(name: name, directory: nil)
}


/// Awesome caching

/**
* Returns a cached object immediately or evaluates a cacheBlock. The cacheBlock is not re-evaluated until the object is expired or manually deleted.
*
* If the cache already contains an object, the completion block is called with the cached object immediately.
*
* If no object is found or the cached object is already expired, the `cacheBlock` is called.
* You might perform any tasks (e.g. network calls) within this block. Upon completion of these tasks, make sure to call the completion block that is passed to the `cacheBlock`.
* The completion block is invoked as soon as the cacheBlock is finished and the object is cached.
*
* @param key The key for the cached object
* @param cacheBlock This block gets called if there is no cached object or this object is already expired.
* The supplied block must be called upon completion (with the object to cache and its expiry).
* @param completaion Called as soon as a cached object is available to use. The second parameter is true if the object was already cached.
*/
func setObjectForKey(key: String, cacheBlock: ((T, AwesomeCacheExpiry) -> ()) -> (), completion: (T, Bool) -> ()) {
if let object = objectForKey(key) {
completion(object, true)
} else {
let cacheReturnBlock: (T, AwesomeCacheExpiry) -> () = { (obj, expires) in
self.setObject(obj, forKey: key, expires: expires)
completion(obj, false)
}
cacheBlock(cacheReturnBlock)
}
}


/// Get object

/**
* Looks up and returns an object with the specified name if it exists.
* If an object is already expired, it is automatically deleted and `nil` will be returned.
*
* @param name The name of the object that should be returned
* @return The cached object for the given name, or nil
*/
func objectForKey(key: String) -> T? {
var possibleObject: AwesomeCacheObject?

// Check if object exists in local cache
possibleObject = cache.objectForKey(key) as? AwesomeCacheObject

if !possibleObject {
// Try to load object from disk (synchronously)
dispatch_sync(diskQueue) {
let path = self._pathForKey(key)
if self.fileManager.fileExistsAtPath(path) {
possibleObject = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as? AwesomeCacheObject
}
}
}

// Check if object is not already expired and return
// Delete object if expired
if let object = possibleObject {
if !object.isExpired() {
return object.value as? T
} else {
removeObjectForKey(key)
}
}

return nil
}


/// Set object

/**
* Adds a given object to the cache.
*
* @param object The object that should be cached
* @param forKey A key that represents this object in the cache
*/
func setObject(object: T, forKey key: String) {
self.setObject(object, forKey: key, expires: .Never)
}

/**
* Adds a given object to the cache.
* The object is automatically marked as expired as soon as its expiry date is reached.
*
* @param object The object that should be cached
* @param forKey A key that represents this object in the cache
*/
func setObject(object: T, forKey key: String, expires: AwesomeCacheExpiry) {
let expiryDate = _expiryDateForCacheExpiry(expires)
let cacheObject = AwesomeCacheObject(value: object, expiryDate: expiryDate)

// Set object in local cache
cache.setObject(cacheObject, forKey: key)

// Write object to disk (asyncronously)
dispatch_async(diskQueue) {
let path = self._pathForKey(key)
NSKeyedArchiver.archiveRootObject(cacheObject, toFile: path)
}
}


/// Remove objects

/**
* Removes an object from the cache.
*
* @param key The key of the object that should be removed
*/
func removeObjectForKey(key: String) {
cache.removeObjectForKey(key)

dispatch_async(diskQueue) {
let path = self._pathForKey(key)
self.fileManager.removeItemAtPath(path, error: nil)
}
}

/**
* Removes all objects from the cache.
*/
func removeAllObjects() {
cache.removeAllObjects()

dispatch_async(diskQueue) {
let paths = self.fileManager.contentsOfDirectoryAtPath(self.directory, error: nil) as [String]
for path in paths {
self.fileManager.removeItemAtPath(path, error: nil)
}
}
}


/// Remove Expired Objects

/**
* Removes all expired objects from the cache.
*/
func removeExpiredObjects() {
dispatch_async(diskQueue) {
let paths = self.fileManager.contentsOfDirectoryAtPath(self.directory, error: nil) as [String]
let keys = paths.map { $0.lastPathComponent.stringByDeletingPathExtension }

for key in keys {
// `objectForKey:` deletes the object if it is expired
self.objectForKey(key)
}
}
}


/// Subscripting

subscript(key: String) -> T? {
get {
return objectForKey(key)
}
set(newValue) {
if let value = newValue {
setObject(value, forKey: key)
} else {
removeObjectForKey(key)
}
}
}


/// @private Helper

/**
* @private
*/
func _pathForKey(key: String) -> String {
return directory.stringByAppendingPathComponent(key).stringByAppendingPathExtension("cache")
}

/**
* @private
*/
func _expiryDateForCacheExpiry(expiry: AwesomeCacheExpiry) -> NSDate {
switch expiry {
case .Never:
return NSDate.distantFuture() as NSDate
case .Seconds(let seconds):
return NSDate().dateByAddingTimeInterval(seconds)
case .Date(let date):
return date
}
}
}

55 changes: 55 additions & 0 deletions AwesomeCache/AwesomeCacheObject.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// CacheObject.swift
// Example
//
// Created by Alexander Schuch on 12/07/14.
// Copyright (c) 2014 Alexander Schuch. All rights reserved.
//

import Foundation

/**
* This class is a wrapper around an objects that should be cached to disk.
*
* NOTE: It is currently not possible to use generics with a subclass of NSObject
* However, NSKeyedArchiver needs a concrete subclass of NSObject to work correctly
*/
class AwesomeCacheObject : NSObject, NSCoding {
let value: AnyObject
let expiryDate: NSDate

/**
* Designated initializer.
*
* @param value An object that should be cached
* @param expiryDate The expiry date of the given value
*/
init(value: AnyObject, expiryDate: NSDate) {
self.value = value
self.expiryDate = expiryDate
}

/**
* Returns true if this object is expired.
* Expiry of the object is determined by its expiryDate.
*/
func isExpired() -> Bool {
let expires = expiryDate.timeIntervalSinceNow
let now = NSDate().timeIntervalSinceNow

return now > expires
}


/// NSCoding

init(coder aDecoder: NSCoder!) {
value = aDecoder.decodeObjectForKey("value")
expiryDate = aDecoder.decodeObjectForKey("expiryDate") as NSDate
}

func encodeWithCoder(aCoder: NSCoder!) {
aCoder.encodeObject(value, forKey: "value")
aCoder.encodeObject(expiryDate, forKey: "expiryDate")
}
}
Loading

0 comments on commit 604b084

Please sign in to comment.