Skip to content

Instantly share code, notes, and snippets.

@guzhenhuaGitHub
Last active September 24, 2023 08:03
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save guzhenhuaGitHub/7e3a422eedd12c2af6f7a4b2028c9d19 to your computer and use it in GitHub Desktop.
Save guzhenhuaGitHub/7e3a422eedd12c2af6f7a4b2028c9d19 to your computer and use it in GitHub Desktop.
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)
}
}
@tangzzz-fan
Copy link

When call saveToDisk? Should we use entry as the main model?

@guzhenhuaGitHub
Copy link
Author

When call saveToDisk? Should we use entry as the main model?

this is a swift version of NSCache. Just use it like NSCache
You can read this article: https://www.swiftbysundell.com/articles/caching-in-swift/

@Vithanco
Copy link

Thanks for this! Great that you put it all together!
How do I load the cache from the disk? I assume there is a function missing, that does the opposite of saveToDisk, or is there a shortcut that I overlook?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment