Cache doesn't claim to be unique in this area, but it's not another monster
library that gives you a god's power. It does nothing but caching, but it does it well. It offers a good public API
with out-of-box implementations and great customization possibilities. Cache utilizes Codable to perform serialization.
- Works with
CodableandSendable. Anything conforming to both will be saved and loaded easily byStorage. - Disk, memory, or hybrid storage modes.
- Many options via
DiskConfigandMemoryConfig. - Support
expiryand clean up of expired objects. - Thread safe.
StorageisSendableand can be accessed from any queue. - Store images via
ImageWrapper. - iOS, tvOS, macOS, watchOS and visionOS support.
Cache is built based on Chain-of-responsibility pattern, in which there are many processing objects, each knows how to do 1 task and delegates to the next one. But that's just implementation detail. All you need to know is Storage, it saves and loads Codable objects.
Storage supports three modes: disk-only, memory-only, or hybrid (memory + disk). Memory storage is fast but volatile, while disk storage persists across application launches.
// Disk only
let diskConfig = DiskConfig(name: "Floppy")
let storage = try Storage(diskConfig: diskConfig)
// Hybrid (memory + disk)
let memoryConfig = MemoryConfig(expiry: .never, countLimit: 10)
let storage = try Storage(diskConfig: diskConfig, memoryConfig: memoryConfig)
// Memory only
let storage = Storage(memoryConfig: MemoryConfig(expiry: .never, countLimit: 50))Storage supports any objects that conform to Codable protocol. You can make your own things conform to Codable so that can be saved and loaded from Storage.
The supported types are
- Primitives like
Int,Float,String,Bool, ... - Array of primitives like
[Int],[Float],[Double], ... - Set of primitives like
Set<String>,Set<Int>, ... - Simply dictionary like
[String: Int],[String: String], ... DateURLData
Error handling is done via try catch. Storage throws errors in terms of StorageError.
public enum StorageError: Error {
/// Object can not be found
case notFound(key: String)
/// Object is found, but casting to requested type failed
case typeNotMatch(key: String)
/// The file attributes are malformed
case malformedFileAttributes(key: String)
/// Can't perform Decode
case decodingFailed(context: String, underlyingError: Error?)
/// Can't perform Encode
case encodingFailed(context: String, underlyingError: Error?)
}There can be errors because of disk problem or type mismatch when loading from storage, so if want to handle errors, you need to do try catch
do {
let storage = try Storage(diskConfig: diskConfig, memoryConfig: memoryConfig)
} catch {
print(error)
}Here is how you can play with many configuration options
let diskConfig = DiskConfig(
// The name of disk storage, this will be used as folder name within directory
name: "Floppy",
// Expiry date that will be applied by default for every added object
// if it's not overridden in the `setObject(forKey:expiry:)` method
expiry: .date(Date().addingTimeInterval(2*3600)),
// Maximum size of the disk cache storage (in bytes)
maxSize: 10000,
// Where to store the disk cache. If nil, it is placed in `cachesDirectory` directory.
directory: try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask,
appropriateFor: nil, create: true).appendingPathComponent("MyPreferences"),
// Data protection is used to store files in an encrypted format on disk and to decrypt them on demand
protectionType: .complete
)let memoryConfig = MemoryConfig(
// Expiry date that will be applied by default for every added object
// if it's not overridden in the `setObject(forKey:expiry:)` method
expiry: .date(Date().addingTimeInterval(2*60)),
/// The maximum number of objects in memory the cache should hold. 0 means no limit.
countLimit: 50
)On iOS, tvOS we can also specify protectionType on DiskConfig to add a level of security to files stored on disk by your app in the app’s container. For more information, see FileProtectionType
Storage is thread safe and Sendable — you can access it from any queue or task. All functions are constrained by the StorageAware protocol.
// Save to storage
try? storage.setObject(10, forKey: "score")
try? storage.setObject("Oslo", forKey: "my favorite city", expiry: .never)
try? storage.setObject(["alert", "sounds", "badge"], forKey: "notifications")
try? storage.setObject(data, forKey: "a bunch of bytes")
try? storage.setObject(authorizeURL, forKey: "authorization URL")
// Load from storage
let score = try? storage.object(ofType: Int.self, forKey: "score")
let favoriteCharacter = try? storage.object(ofType: String.self, forKey: "my favorite city")
// Check if an object exists
let hasFavoriteCharacter = try? storage.existsObject(forKey: "my favorite city")
// Remove an object in storage
try? storage.removeObject(forKey: "my favorite city")
// Remove all objects
try? storage.removeAll()
// Remove expired objects
try? storage.removeExpiredObjects()There is time you want to get object together with its expiry information and meta data. You can use Entry
let entry = try? storage.entry(ofType: String.self, forKey: "my favorite city")
print(entry?.object)
print(entry?.expiry)
print(entry?.meta)meta may contain file information if the object was fetched from disk storage.
Types stored in Storage must conform to both Codable and Sendable. It does not work for [String: Any] as Any conforms to neither. Convert JSON responses to strongly typed objects before saving.
struct User: Codable, Sendable {
let firstName: String
let lastName: String
}
let user = User(firstName: "John", lastName: "Snow")
try? storage.setObject(user, forKey: "character")By default, all saved objects have the same expiry as the expiry you specify in DiskConfig or MemoryConfig. You can overwrite this for a specific object by specifying expiry for setObject
// Default expiry date from configuration will be applied to the item
try? storage.setObject("This is a string", forKey: "string")
// A given expiry date will be applied to the item
try? storage.setObject(
"This is a string",
forKey: "string",
expiry: .date(Date().addingTimeInterval(2 * 3600))
)
// Clear expired objects
try? storage.removeExpiredObjects()As you may know, NSImage and UIImage don't conform to Codable by default. To make it play well with Codable, we introduce ImageWrapper, so you can save and load images like
let wrapper = ImageWrapper(image: starIconImage)
try? storage.setObject(wrapper, forKey: "star")
let icon = try? storage.object(ofType: ImageWrapper.self, forKey: "star").imageIf you want to load image into UIImageView or NSImageView, then we also have a nice gift for you. It's called Imaginary and uses Cache under the hood to make you life easier when it comes to working with remote images.
- Original idea: Hyper made this with ❤️
- Reworked, simplified and modernized: Gabor S
Cache is available under the MIT license.
Bug fixes • Fixed LRU eviction sorting (was evicting newest instead of oldest) • Fixed Expiry.never using hardcoded 68-year date → Date.distantFuture • Fixed object(ofType:) returning expired entries → now throws notFound • Fixed SyncStorage forced unwrap crash pattern • Fixed fileManager.createFile ignoring failure → now throws on false
Modernization • Adopted async/await, raised platform minimums to iOS 16+/macOS 13+ • Replaced 280-line hand-rolled MD5 with CryptoKit Insecure.MD5 • Replaced NSString(string:) allocations with free as NSString bridging
Thread safety • Added OSAllocatedUnfairLock to MemoryStorage, DiskStorage, HybridStorage • Made entire storage chain genuinely Sendable (eliminated all @unchecked Sendable on storage types) • Made MemoryCapsule final, private, @unchecked Sendable with any Sendable instead of Any
Removed dead code/redundant layers • Deleted SyncStorage, AsyncStorage, AsyncStorageAware, Result.swift, ExpirationMode.swift, JSONDecoder+Extensions.swift • Removed unused totalSize() and removeObjectIfExpired() from DiskStorage • Removed deprecated MemoryConfig initializer with totalCostLimit
API improvements • Enriched StorageError with associated values (key, context, underlyingError) • Added Storage.init(memoryConfig:) for memory-only caching • Made StorageAware constraints consistently Codable & Sendable • Made JSONDictionaryWrapper Sendable • Changed DataSerializer from class to enum with static encoder/decoder
Code quality • Replaced force casts with conditional casts in DiskStorage • Tightened access control (private over fileprivate, consolidated private extensions) • Renamed MD5.MD5() → MD5.hash(), fixed parameter shadowing (remainingSize) • Made StorageAware internal, removed public from TypeWrapperStorage methods • Fixed stale comment in NSImage+Extensions
README • Updated to reflect all changes: removed async/Alamofire/SwiftHash sections, updated StorageError/MemoryConfig examples, documented three storage modes, fixed typos