-
-
Save karthiikmk/48590e25c5a4b8fe4abcdbb79cbe032e to your computer and use it in GitHub Desktop.
This file contains hidden or 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
import UIKit | |
// According to article by John Sundell | |
// https://www.swiftbysundell.com/articles/caching-in-swift/ | |
final class Cache<Key: Hashable, Value> { | |
private let wrapped = NSCache<WrappedKey, Entry>() | |
private let dateProvider: () -> Date | |
private let entryLifetime: TimeInterval | |
private let keyTracker = KeyTracker() | |
init(dateProvider: @escaping () -> Date = Date.init, | |
entryLifetime: TimeInterval = 12 * 60 * 60, | |
maximumEntryCount: Int = 50) { | |
self.dateProvider = dateProvider | |
self.entryLifetime = entryLifetime | |
wrapped.countLimit = maximumEntryCount | |
wrapped.delegate = keyTracker | |
} | |
func insert(_ value: Value, forKey key: Key) { | |
let date = dateProvider().addingTimeInterval(entryLifetime) | |
let entry = Entry(key: key, value: value, expirationDate: date) | |
wrapped.setObject(entry, forKey: WrappedKey(key)) | |
keyTracker.keys.insert(key) | |
} | |
func value(forKey key: Key) -> Value? { | |
guard let entry = wrapped.object(forKey: WrappedKey(key)) else { | |
return nil | |
} | |
guard dateProvider() < entry.expirationDate else { | |
// Discard values that have expired | |
removeValue(forKey: key) | |
return nil | |
} | |
return entry.value | |
} | |
func removeValue(forKey key: Key) { | |
wrapped.removeObject(forKey: WrappedKey(key)) | |
} | |
} | |
// MARK: - Cache Subscript | |
extension Cache { | |
subscript(key: Key) -> Value? { | |
get { return value(forKey: key) } | |
set { | |
guard let value = newValue else { | |
// If nil was assigned using our subscript, | |
// then we remove any value for that key: | |
removeValue(forKey: key) | |
return | |
} | |
insert(value, forKey: key) | |
} | |
} | |
} | |
// MARK: - Cache.WrappedKey | |
private extension Cache { | |
final class WrappedKey: NSObject { | |
let key: Key | |
init(_ key: Key) { self.key = key } | |
override var hash: Int { return key.hashValue } | |
override func isEqual(_ object: Any?) -> Bool { | |
guard let value = object as? WrappedKey else { | |
return false | |
} | |
return value.key == key | |
} | |
} | |
} | |
// MARK: - Cache.Entry | |
private extension Cache { | |
final class Entry { | |
let key: Key | |
let value: Value | |
let expirationDate: Date | |
init(key: Key, value: Value, expirationDate: Date) { | |
self.key = key | |
self.value = value | |
self.expirationDate = expirationDate | |
} | |
} | |
} | |
// MARK: - Cache.KeyTracker | |
private extension Cache { | |
final class KeyTracker: NSObject, NSCacheDelegate { | |
var keys = Set<Key>() | |
func cache(_ cache: NSCache<AnyObject, AnyObject>, | |
willEvictObject obj: Any) { | |
guard let entry = obj as? Entry else { | |
return | |
} | |
keys.remove(entry.key) | |
} | |
} | |
} | |
// MARK: - Cache Codable | |
extension Cache.Entry: Codable where Key: Codable, Value: Codable {} | |
private extension Cache { | |
func entry(forKey key: Key) -> Entry? { | |
guard let entry = wrapped.object(forKey: WrappedKey(key)) else { | |
return nil | |
} | |
guard Date() < entry.expirationDate else { | |
removeValue(forKey: key) | |
return nil | |
} | |
return entry | |
} | |
func insert(_ entry: Entry) { | |
wrapped.setObject(entry, forKey: WrappedKey(entry.key)) | |
keyTracker.keys.insert(entry.key) | |
} | |
} | |
extension Cache: Codable where Key: Codable, Value: Codable { | |
convenience init(from decoder: Decoder) throws { | |
self.init() | |
let container = try decoder.singleValueContainer() | |
let entries = try container.decode([Entry].self) | |
entries.forEach(insert) | |
} | |
func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
try container.encode(keyTracker.keys.compactMap(entry)) | |
} | |
} | |
// MARK: - Cache Save To Disk | |
extension Cache where Key: Codable, Value: Codable { | |
func saveToDisk( | |
with name: String, | |
using fileManager: FileManager = .default | |
) throws { | |
let folderURLs = fileManager.urls( | |
for: .cachesDirectory, | |
in: .userDomainMask | |
) | |
let fileURL = folderURLs[0].appendingPathComponent(name + ".cache") | |
let data = try JSONEncoder().encode(self) | |
try data.write(to: fileURL) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment