Cache

repository·master·Indexed 25 days ago

https://github.com/hyperoslo/cache

A lightweight, thread-safe Swift caching library for iOS, tvOS, and macOS. It utilizes Codable for serialization and supports hybrid memory and disk storage, object expiry, and both synchronous and asynchronous APIs. Features include customizable DiskConfig and MemoryConfig, type-safe Storage instances via transformers, and a token-based observation system for monitoring storage changes.

Tokens
3.1K
Snippets
11
Records
15
Agent score
35%

What's inside hyperoslo-cache

  1. Overview of Cache

    master
    Cache is a Swift-based caching library designed for simplicity and performance. It focuses exclusively on caching, providing a clean public API with out-of-the-box implementations and high customization. It leverages Swift's Codable protocol for seamless serialization of any conforming type.
  2. Key features of Cache

    master

    Cache provides the following capabilities:

    • Codable Support: Works with any type conforming to Codable for easy saving and loading.
    • Hybrid Storage: Supports both memory and disk storage.
    • Customizable Configuration: Extensive options via DiskConfig and MemoryConfig.
    • Expiry Management: Supports object expiry and automatic cleanup of expired items.
    • Thread Safety: All operations are thread-safe and can be accessed from any queue.
    • API Styles: Synchronous APIs by default, with support for Asynchronous APIs.
    • Platform Support: Compatible with iOS, tvOS, and macOS.
  3. Initialize Storage with Disk and Memory configurations

    master

    To create a Storage instance, provide DiskConfig and MemoryConfig. The Storage class uses a HybridStorage approach by default, combining fast in-memory access with persistent disk storage. You can also provide a transformer using TransformerFactory to handle type serialization (e.g., for Codable types).

    let diskConfig = DiskConfig(name: "Floppy")
    let memoryConfig = MemoryConfig(expiry: .never, countLimit: 10, totalCostLimit: 10)
    
    let storage = try? Storage(
      diskConfig: diskConfig,
      memoryConfig: memoryConfig,
      transformer: TransformerFactory.forCodable(ofType: User.self) // Storage<String, User>
    )
  4. Handle and persist JSON responses

    master

    When fetching JSON from a backend (e.g., using Alamofire), it is recommended to decode the JSON into strongly typed objects before saving them to Storage. Storage can persist String or Data, or you can use JSONArrayWrapper and JSONDictionaryWrapper for JSON. However, persisting strongly typed objects is preferred for UI consistency.

    Use the JSONDecoder extensions to decode String, Dictionary, or Data into your models:

    let user = JSONDecoder.decode(jsonString, to: User.self)
    let cities = JSONDecoder.decode(jsonDictionary, to: [City].self)
    let dragons = JSONDecoder.decode(jsonData, to: [Dragon].self)

    Example workflow with Alamofire:

    Alamofire.request("https://gameofthrones.org/mostFavoriteCharacter").responseString { response in
      do {
        let user = try JSONDecoder.decode(response.result.success, to: User.self)
        try storage.setObject(user, forKey: "most favorite character")
      } catch {
        print(error)
      }
    }
  5. Configure DiskConfig options

    master

    Use DiskConfig to customize disk persistence:

    • name: The folder name within the directory.
    • expiry: Default expiry date for added objects.
    • maxSize: Maximum size of the disk cache in bytes.
    • directory: Custom URL for storage (defaults to cachesDirectory).
    • protectionType: iOS/tvOS specific FileProtectionType for encryption.
    let diskConfig = DiskConfig(
      name: "Floppy",
      expiry: .date(Date().addingTimeInterval(2*3600)),
      maxSize: 10000,
      directory: try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask,
        appropriateFor: nil, create: true).appendingPathComponent("MyPreferences"),
      protectionType: .complete
    )
  6. Configure MemoryConfig options

    master

    Use MemoryConfig to customize in-memory caching:

    • expiry: Default expiry date for added objects.
    • countLimit: Maximum number of objects allowed in memory.
    • totalCostLimit: Maximum total cost (e.g., bytes/size) before eviction starts.
    let memoryConfig = MemoryConfig(
      expiry: .date(Date().addingTimeInterval(2*60)),
      countLimit: 50,
      totalCostLimit: 0
    )
  7. Use Synchronous Storage APIs

    master

    Synchronous APIs are thread-safe and can be accessed from any queue. Use try? or do-catch to handle potential StorageErrors.

    // Save to storage
    try? storage.setObject(10, forKey: "score")
    try? storage.setObject("Oslo", forKey: "my favorite city", expiry: .never)
    
    // Load from storage
    let score = try? storage.object(forKey: "score")
    
    // Check existence
    let hasFavoriteCharacter = try? storage.objectExists(forKey: "my favorite city")
    
    // Remove items
    try? storage.removeObject(forKey: "my favorite city")
    try? storage.removeAll()
    try? storage.removeExpiredObjects()
    
    // Get entry with metadata
    let entry = try? storage.entry(forKey: "my favorite city")
    print(entry?.object)
    print(entry?.expiry)
    print(entry?.meta)
  8. Transform Storage types using Transformers

    master

    All Storage instances are generic and type-safe. Once created, the type constraint is maintained for all subsequent operations. You can create new Storage instances with different type annotations from an existing one using transformation methods like transformImage() or transformCodable(ofType:). This allows you to work with different types while sharing the same underlying caching mechanism.

    let storage: Storage<String, User> = ...
    storage.setObject(superman, forKey: "user")
    
    let imageStorage = storage.transformImage() // Storage<String, UIImage>
    imageStorage.setObject(image, forKey: "image")
    
    let stringStorage = storage.transformCodable(ofType: String.self) // Storage<String, String>
    stringStorage.setObject("hello world", forKey: "string")
  9. Use Asynchronous Storage APIs

    master

    Access asynchronous APIs via the .async property. These return a Result type in a completion block or support Swift Concurrency (async/await).

    // Completion handler pattern
    storage.async.setObject("Oslo", forKey: "my favorite city") { result in
      switch result {
      case .success: print("saved successfully")
      case .failure(let error): print(error)
      }
    }
    
    // Swift Concurrency pattern
    do {
      try await storage.async.setObject("Oslo", forKey: "my favorite city")
      print("saved successfully")
    } catch {
      print(error)
    }