-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathCacheService.swift
319 lines (278 loc) · 14.1 KB
/
CacheService.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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
//
// CacheService.swift
import Foundation
import UIKit
//you can't cast objects as protocols, so instead we are extending concerete objects to conform to protocols
extension Encodable {
func toJSONData() -> Data? { try? JSONEncoder().encode(self) }
}
extension Encodable {
func toJSONString() -> String? {
guard let data = self.toJSONData() else {
return nil
}
return String(data: data, encoding: String.Encoding.utf8) }
}
extension Decodable {
init(jsonData: Data) throws {
self = try JSONDecoder().decode(Self.self, from: jsonData)
}
}
class CacheService {
public static let shared = CacheService();
//if we are using sync and barriers everywhere why bother with .concurrent? anyway, research later
private let queue = DispatchQueue(label: "service.cache", attributes: .concurrent)
private init() {
loadCacheFromDisk()
}
private var NSObjectCache = Dictionary<String, NSCacheObject>()
private var CodableCache = Dictionary<String, CodableCacheObject>()
private let NSObjectCacheFileName = "nsobjectcache.dat"
private let CodableCacheFileName = "codablecache.dat"
func getCount() -> Int {
return NSObjectCache.count + CodableCache.count
}
func clearCache(memoryOnly: Bool = false) {
NSObjectCache = Dictionary<String, NSCacheObject>()
CodableCache = Dictionary<String, CodableCacheObject>()
print("Cleared memory cache.")
if !memoryOnly {
FileSystem.clearCacheDirectory()
}
}
func purgeExpiredObjects() {
let purgeDate = Date()
for (key, value) in self.CodableCache {
if value.expires == true && value.expirationDate <= purgeDate {
self.CodableCache.removeValue(forKey: key)
}
}
for (key, value) in self.NSObjectCache {
if value.expires == true && value.expirationDate <= purgeDate {
self.NSObjectCache.removeValue(forKey: key)
}
}
}
func saveCacheToDisk() {
//save codable cache - could probably just serialize the dictionary itself, but alas:
var codablediskcache:[CodableCacheObject] = [];
for (_, value) in self.CodableCache {
codablediskcache.append(value)
}
let jsonEncoder = JSONEncoder()
guard let codableCacheData = try? jsonEncoder.encode(codablediskcache) else {
print("Error archiving codable cache: Encoding of CodableCache resulted in nil data.")
return;
}
FileSystem.writeToCacheDirectory(data: codableCacheData, fileName: CodableCacheFileName)
//save nsobject cache
let objectdiskcache = NSCacheObjectList()
for (_, value) in self.NSObjectCache {
objectdiskcache.list.append(value)
}
do {
let objectCacheData = try NSKeyedArchiver.archivedData(withRootObject: objectdiskcache, requiringSecureCoding: false)
FileSystem.writeToCacheDirectory(data: objectCacheData, fileName: NSObjectCacheFileName)
} catch {
print("Error: Could not archive NSCacheObjectList to disk. Error: \(error.localizedDescription)")
}
}
public func loadCacheFromDisk() {
let loadDate = Date()
//load codables:
if let loadedData = FileSystem.readFromCacheDirectory(fileName: CodableCacheFileName) {
do {
let jsonDecoder = JSONDecoder()
if let diskcache = try jsonDecoder.decode([CodableCacheObject]?.self, from: loadedData) {
print("Successfully unarchived CodableObjectCache: \(diskcache.count) items")
for entry in diskcache {
if entry.expires == false || entry.expirationDate > loadDate {
CodableCache[entry.key] = entry;
}
}
print("\(CodableCache.count) items are now in CodableObjectCache (if count is lower than above, some items from archive may have expired).")
} else {
print("Error: Could not unarchive CodableObjectCache from disk. Could not unarchive object, or archived object was null.")
FileSystem.clearCacheDirectory() //WARNING: should probably pass in file names so we dont blow out other specific caches, but leaving this for now, since we will most likely just keep the monolith cache system instance
}
} catch {
print("Error: Could not unarchive CodableObjectCache from disk. Error: \(error.localizedDescription)")
FileSystem.clearCacheDirectory() //WARNING: should probably pass in file names so we dont blow out other specific caches, but leaving this for now, since we will most likely just keep the monolith cache system instance
}
}
//load NSobjects:
if let loadedData = FileSystem.readFromCacheDirectory(fileName: NSObjectCacheFileName) {
do {
//NSKeyedUnarchiver.unarchivedObject(ofClass: NSCacheObjectList.self, from: loadedData)
//NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(loadedData) as? NSCacheObjectList
//NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSCacheObjectList.self, NSCacheObject.self, UIImage.self, NSArray.self], from: loadedData) as? NSCacheObjectList
if let diskcache = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(loadedData) as? NSCacheObjectList {
print("Successfully unarchived NSObjectCache: \(diskcache.list.count) items")
for entry in diskcache.list {
if entry.expires == false || entry.expirationDate > loadDate {
NSObjectCache[entry.key] = entry;
}
}
print("\(NSObjectCache.count) items are now in NSObjectCache (if count is lower than above, some items from archive may have expired).")
} else {
print("Error: Could not unarchive NSObjectCache from disk. Could not unarchive object, or archived object was null.")
FileSystem.clearCacheDirectory() //WARNING: should probably pass in file names so we dont blow out other specific caches, but leaving this for now, since we will most likely just keep the monolith cache system instance
}
} catch {
print("Error: Could not unarchive NSObjectCache from disk. Error: \(error.localizedDescription)")
FileSystem.clearCacheDirectory() //WARNING: should probably pass in file names so we dont blow out other specific caches, but leaving this for now, since we will most likely just keep the monolith cache system instance
}
}
}
//WARNING: ALL nscache objects need to follow the NSObject, NSCoding standard in order to serialize properly.
//So why are we doing this? Codable only works on almalgamations of primitive types, and swift can't serialize general AnyObjects yet, while NSObject can, provided you implement NSCoding (and images happen to serialize perfectly, as they already adhere to NSCoding).
//update: casting [Codable] to AnyObject throws it's type away
func cacheObject<T>(key: String, object: T, expires:Bool = true, TTLInMinutes:Int? = nil)
{
var newTTL = TTLInMinutes
if newTTL == nil {
if object is UIImage {
newTTL = 1440 //cache images for a day
} else {
newTTL = 10 //cache anything else for 10 MINUTES
}
}
if object is Codable {
if let objectAsJson = (object as! Codable).toJSONString() {
let codableCacheObj = CodableCacheObject(key: key, expires: expires, expirationDate: Date().addingTimeInterval(TimeInterval(newTTL! * 60)), stringSerializedObject: objectAsJson)
queue.sync(flags: .barrier) {
self.CodableCache[key] = codableCacheObj;
//print("cached data for: \(key)")
}
} else {
print("could not serialize codable object for: \(key)")
}
} else {
let cacheObj = NSCacheObject(key: key, expires: expires, expirationDate: Date().addingTimeInterval(TimeInterval(newTTL! * 60)), object: object as AnyObject)
queue.sync(flags: .barrier) {
self.NSObjectCache[key] = cacheObj;
//print("cached data for: \(key)")
}
}
}
func getObject<T>(for key:String) -> T? {
queue.sync(flags: .barrier) {
if T.self is Codable.Type {
if let cachedObject = CodableCache[key] {
//should be in UTC to deal with a change in timezone.... unless that is handled under the hood. dont care right now
if cachedObject.expires == false || cachedObject.expirationDate > Date() {
guard let model = T.self as? Decodable.Type else {
CodableCache.removeValue(forKey: key)
print("could not convert codable cached object to type \(T.self) for: \(key)")
return nil as T?;
}
if let returnObj = try? model.init(jsonData: cachedObject.stringSerializedObject.data(using: .utf8)!) as? T {
//print("Found and returned good object for key: \(key)")
return returnObj;
} else {
CodableCache.removeValue(forKey: key)
print("could not convert codable cached object to type \(T.self) for: \(key)")
return nil as T?;
}
} else {
CodableCache.removeValue(forKey: key)
print("cache expired for codable key (removing now): \(key)")
return nil as T?;
}
} else {
//print("object does not exist in cache for key: \(key)")
return nil as T?;
}
} else {
if let cachedObject = NSObjectCache[key] {
//should be in UTC to deal with a change in timezone.... unless that is handled under the hood. dont care right now
if cachedObject.expires == false || cachedObject.expirationDate > Date() {
if let returnObj = cachedObject.object as? T {
//print("Found and returned good object for key: \(key)")
return returnObj;
} else {
NSObjectCache.removeValue(forKey: key)
print("could not convert nsobject cached object to type \(T.self) for: \(key)")
return nil as T?;
}
} else {
NSObjectCache.removeValue(forKey: key)
print("cache expired for nsobject key (removing now): \(key)")
return nil as T?;
}
} else {
//print("object does not exist in cache for key: \(key)")
return nil as T?;
}
}
}
}
deinit {
saveCacheToDisk()
}
}
class CodableCacheObject : Codable {
var key: String
var expires:Bool
var expirationDate: Date
var stringSerializedObject: String
init (key: String, expires:Bool, expirationDate:Date, stringSerializedObject: String) {
self.key = key
self.expires = expires
self.expirationDate = expirationDate
self.stringSerializedObject = stringSerializedObject
}
}
class NSCacheObjectList : NSObject, NSSecureCoding {
static var supportsSecureCoding: Bool = true
var list: [NSCacheObject]
init (list: [NSCacheObject]? = nil) {
if list == nil {
self.list = [NSCacheObject]()
} else {
self.list = list!
}
super.init()
}
required convenience init?(coder aDecoder: NSCoder) {
guard let items = aDecoder.decodeObject(forKey: "list") as? [NSCacheObject] else {
return nil
}
self.init(list: items)
}
func encode(with aCoder: NSCoder) {
aCoder.encode(list, forKey: "list")
}
}
//WARNING: ALL nscache objects need to follow the NSObject, NSCoding standard in order to serialize properly.
//So why are we doing this? Codable only works on primitive types, and swift can't serialize general AnyObjects yet, while NSObject can, provided you implement NSCoding (and images happen to serialize perfectly, as they already adhere to NSCoding).
class NSCacheObject : NSObject, NSSecureCoding {
static var supportsSecureCoding: Bool = true
var key: String
var expires:Bool
var expirationDate: Date
var object: AnyObject
init (key: String, expires:Bool, expirationDate:Date, object: AnyObject) {
self.key = key
self.expires = expires
self.expirationDate = expirationDate
self.object = object
super.init()
}
required convenience init?(coder aDecoder: NSCoder) {
guard let k = aDecoder.decodeObject(forKey: "key") as? String,
let expiry = aDecoder.decodeObject(forKey: "expirationDate") as? Date,
let val = aDecoder.decodeObject(forKey: "object")
else {
return nil
}
let exp = aDecoder.decodeBool(forKey: "expires")
self.init(key: k, expires: exp, expirationDate: expiry, object: val as AnyObject)
}
func encode(with aCoder: NSCoder) {
aCoder.encode(key, forKey: "key")
aCoder.encode(expirationDate, forKey: "expirationDate")
aCoder.encode(expires, forKey: "expires")
aCoder.encode(object, forKey: "object")
}
}