Skip to content

Instantly share code, notes, and snippets.

@jancassio
Last active November 23, 2021 00:41
Show Gist options
  • Save jancassio/9d2c66dd911baefb462b3174938bff29 to your computer and use it in GitHub Desktop.
Save jancassio/9d2c66dd911baefb462b3174938bff29 to your computer and use it in GitHub Desktop.
Cache is a NSCache that allows to store value types in memory.

Cache

Allows to store values in memory, powered by system NSCache (but for value types too). The Cache class is full based in a John Sundell tutorial (check out at See Also section).

Also, there's a Environment modifier to allow share cache between views in same context. It would be helpful in situations where Cache is initialized just once in runtime.

And remember, Cache is only cache-memory, so use it wisely.

For file cache based, I trully recommend reading the article mentioned at See Also to understand and implement file-based cache.

Since it brings more complexibility and enable more issues, I'm only dealing with memory in this one to keep things more simple.

Usage

Notice in the example below, cache should be provided at initialization. This allows to share cache outside the Loader and enables easy unit testing.

final class SomeDataLoader {
    private let cache: Cache<URL, [SomeModel]>
    private let queue = DispatchQueue(label: "com.myapp.AppName:SomeDataLoader.queue")
    private let session: URLSession
    
    init(withSession session: URLSession = .shared, cache: Cache<URL, [SomeModel]> = .init()) {
        self.session = session
        self.cache = cache
    }
    
    func load(fromURL url: URL) -> AnyPublisher<[SomeModel], Error> {
        // first, try to obtain the current url from the cache
        if let cached = cache[url] {
            return Just<[SomeModel]>(cached)
                .setFailureType(to: Error.self)
                .eraseToAnyPublisher()
        }

      // otherwhise, perform the networking operation
        return session
            .dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: [SomeModel].self, decoder: JSONDecoder())
            .receive(on: queue)
            // this is a side-effect that stores the obtained value in cache
            .handleEvents(receiveOutput: { [weak self] articles in
                self?.cache.set(articles, forKey: url)
            })
            .share()
            .eraseToAnyPublisher()
    }
}

See Also

Caching in Swift is the article that Cache was full inspired, (mostly a copy with a few improvements).

License

MIT

/*
Cache.swift
MIT License
Copyright (c) 2021 Jan Cassio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import Foundation
import SwiftUI
final class Cache<Key: Hashable, Value> {
private let cache = NSCache<KeyWrapper, Entry>()
private let currentDate: () -> Date
private let expireAt: TimeInterval
init(expireAt: TimeInterval = 60 * 60 * 1, currentDate: @escaping () -> Date = Date.init) {
self.expireAt = expireAt
self.currentDate = currentDate
}
func set(_ value: Value, forKey key: Key) {
let expireDate = currentDate().addingTimeInterval(expireAt)
cache.setObject(
Entry(value, expireDate: expireDate),
forKey: KeyWrapper(key)
)
}
func get(_ key: Key) -> Value? {
guard let entry = cache.object(forKey: KeyWrapper(key)) else {
return nil
}
guard currentDate() < entry.expireDate else {
del(key)
return nil
}
return entry.value
}
func del(_ key: Key) {
cache.removeObject(forKey: KeyWrapper(key))
}
}
extension Cache {
subscript(key: Key) -> Value? {
get { get(key) }
set {
guard let value = newValue else {
del(key)
return
}
set(value, forKey: key)
}
}
}
private extension Cache {
final class KeyWrapper: NSObject {
let key: Key
init(_ key: Key) {
self.key = key
}
override var hash: Int {
key.hashValue
}
override func isEqual(_ object: Any?) -> Bool {
guard let object = object as? KeyWrapper else {
return false
}
return object.key == key
}
}
}
private extension Cache {
final class Entry {
let value: Value
let expireDate: Date
init(_ value: Value, expireDate: Date) {
self.value = value
self.expireDate = expireDate
}
}
}
struct CacheInjector: EnvironmentKey {
let articlesCache: Cache<URL, [Article]>
static var defaultValue: Self = .init(
articlesCache: Cache<URL, [Article]>()
)
}
extension EnvironmentValues {
var cache: CacheInjector {
get { self[CacheInjector.self] }
set { self[CacheInjector.self] = newValue }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment