-
Notifications
You must be signed in to change notification settings - Fork 168
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 604b084
Showing
19 changed files
with
1,699 additions
and
0 deletions.
There are no files selected for viewing
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
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 |
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
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 |
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
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 | ||
} | ||
} | ||
} | ||
|
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
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") | ||
} | ||
} |
Oops, something went wrong.