forked from aschuch/AwesomeCache
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathCache.swift
266 lines (222 loc) · 7.52 KB
/
Cache.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
//
// 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 CacheExpiry {
case Never
case InSeconds(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 Cache<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.
*
* First looks up an object with the given key. If no object was found or the cached object is already expired, the `cacheBlock` is called.
* You can 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 called immediately, if the cache already contains an object for the given key. Otherwise it is called as soon as the `cacheBlock` completes 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, CacheExpiry) -> ()) -> (), completion: (T, Bool) -> ()) {
if let object = objectForKey(key) {
completion(object, true)
} else {
let cacheReturnBlock: (T, CacheExpiry) -> () = { (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: CacheObject?
// Check if object exists in local cache
possibleObject = cache.objectForKey(key) as? CacheObject
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? CacheObject
}
}
}
// 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: CacheExpiry) {
let expiryDate = _expiryDateForCacheExpiry(expires)
let cacheObject = CacheObject(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: CacheExpiry) -> NSDate {
switch expiry {
case .Never:
return NSDate.distantFuture() as NSDate
case .InSeconds(let seconds):
return NSDate().dateByAddingTimeInterval(seconds)
case .Date(let date):
return date
}
}
}