Cache (Swift)

SwiftLibraryCacheiOSmacOSSPMMemoryDisk

Library

Cache (Swift)

Overview

Cache is a modern Swift caching library that supports Swift Package Manager. It provides caching solutions for iOS and macOS supporting both memory and disk caching with time limits and capacity limits.

Details

Cache is a caching library for modern Swift development that's gaining attention due to the widespread adoption of Swift Package Manager. It supports caching on both memory and disk, providing TTL (Time To Live) time limits and capacity limit functionality. Compatible with iOS 9/macOS 10.11 and later, it can cache custom types conforming to the Codable protocol and primitive types (String, Int, etc.). Integration with the Swift Package Manager (SPM) ecosystem allows natural incorporation into modern Swift development workflows, developed by 0xLeif. Compared to other Swift cache libraries like NSCache and HanekeSwift, it features SPM native support and design leveraging modern Swift language features.

Advantages and Disadvantages

Advantages

  • Swift Package Manager Support: Complete integration with modern Swift development environment
  • Memory & Disk Support: Flexible cache strategy implementation
  • Codable Support: Type-safe cache operations and Swift-native serialization
  • Time & Capacity Limits: Automatic management with TTL and capacity limits
  • iOS/macOS Support: Usage across Apple platforms
  • Simple API: Intuitive and easy-to-use interface
  • Lightweight Design: Minimal dependencies and memory footprint

Disadvantages

  • Apple Platform Only: Cannot be used outside iOS and macOS
  • New Library: Less proven track record compared to mature libraries
  • Limited Features: Advanced cache features inferior to specialized libraries
  • Community Size: Smaller community compared to standard libraries like NSCache
  • Documentation: Limited comprehensive documentation

Key Links

Code Examples

Installation with Swift Package Manager

// Package.swift
import PackageDescription

let package = Package(
    name: "MyProject",
    dependencies: [
        .package(url: "https://github.com/0xLeif/Cache", from: "1.0.0")
    ],
    targets: [
        .target(
            name: "MyProject",
            dependencies: ["Cache"]
        )
    ]
)

Basic Cache Usage

import Cache

// Create Cache instance
let cache = Cache<String, String>()

// Set value
cache.set("user:123", value: "John Doe")

// Get value
if let userName = cache.get("user:123") {
    print("User name: \(userName)")
}

// Remove value
cache.remove("user:123")

// Clear cache
cache.clear()

Cache with TTL (Time To Live)

import Cache

// TTL-enabled cache
let cache = Cache<String, String>(defaultTTL: 300) // 5 minutes

// Set value with TTL
cache.set("temporary_data", value: "This will expire", ttl: 60) // Expires in 1 minute

// Check expiration
if cache.isExpired("temporary_data") {
    print("Data has expired")
} else {
    let data = cache.get("temporary_data")
    print("Data: \(data ?? "nil")")
}

Caching Codable Types

import Cache

// Codable-compliant structure
struct User: Codable {
    let id: String
    let name: String
    let email: String
    let createdAt: Date
}

// Cache for User objects
let userCache = Cache<String, User>()

// Cache user object
let user = User(
    id: "123",
    name: "Jane Smith",
    email: "[email protected]",
    createdAt: Date()
)

userCache.set("user:\(user.id)", value: user, ttl: 3600) // 1 hour

// Retrieve user object
if let cachedUser = userCache.get("user:123") {
    print("Cached user: \(cachedUser.name)")
}

Cache Size Limits

import Cache

// Cache with maximum capacity limit
let cache = Cache<String, Data>(maxSize: 100) // Max 100 items

// Check behavior on capacity overflow
for i in 1...150 {
    let key = "item:\(i)"
    let data = "Data for item \(i)".data(using: .utf8)!
    cache.set(key, value: data)
}

print("Cache size: \(cache.count)") // 100 (max capacity)
print("First item exists: \(cache.get("item:1") != nil)") // false (removed)
print("Last item exists: \(cache.get("item:150") != nil)") // true

Asynchronous Cache Operations

import Cache

class AsyncCacheService {
    private let cache = Cache<String, Data>()
    private let queue = DispatchQueue(label: "cache.queue", attributes: .concurrent)
    
    func setData(_ data: Data, forKey key: String, completion: @escaping () -> Void) {
        queue.async(flags: .barrier) {
            self.cache.set(key, value: data)
            DispatchQueue.main.async {
                completion()
            }
        }
    }
    
    func getData(forKey key: String, completion: @escaping (Data?) -> Void) {
        queue.async {
            let data = self.cache.get(key)
            DispatchQueue.main.async {
                completion(data)
            }
        }
    }
    
    func clearCache(completion: @escaping () -> Void) {
        queue.async(flags: .barrier) {
            self.cache.clear()
            DispatchQueue.main.async {
                completion()
            }
        }
    }
}

// Usage example
let cacheService = AsyncCacheService()

cacheService.setData("Hello, Cache!".data(using: .utf8)!, forKey: "greeting") {
    print("Data cached successfully")
}

cacheService.getData(forKey: "greeting") { data in
    if let data = data, let string = String(data: data, encoding: .utf8) {
        print("Retrieved: \(string)")
    }
}

Hybrid Memory-Disk Cache

import Cache
import Foundation

class HybridCache<Key: Hashable & Codable, Value: Codable> {
    private let memoryCache = Cache<Key, Value>(maxSize: 50) // Memory max 50 items
    private let diskCacheURL: URL
    
    init(diskCacheDirectory: String = "HybridCache") {
        let documentsPath = FileManager.default.urls(for: .documentDirectory, 
                                                   in: .userDomainMask).first!
        self.diskCacheURL = documentsPath.appendingPathComponent(diskCacheDirectory)
        
        // Create directory
        try? FileManager.default.createDirectory(at: diskCacheURL, 
                                                withIntermediateDirectories: true)
    }
    
    func set(_ key: Key, value: Value, ttl: TimeInterval? = nil) {
        // Save to memory cache
        memoryCache.set(key, value: value, ttl: ttl)
        
        // Save to disk cache
        saveToDisk(key: key, value: value)
    }
    
    func get(_ key: Key) -> Value? {
        // Try memory first
        if let value = memoryCache.get(key) {
            return value
        }
        
        // Load from disk and restore to memory
        if let value = loadFromDisk(key: key) {
            memoryCache.set(key, value: value)
            return value
        }
        
        return nil
    }
    
    private func saveToDisk(key: Key, value: Value) {
        do {
            let data = try JSONEncoder().encode(value)
            let url = diskCacheURL.appendingPathComponent("\(key).cache")
            try data.write(to: url)
        } catch {
            print("Failed to save to disk: \(error)")
        }
    }
    
    private func loadFromDisk(key: Key) -> Value? {
        do {
            let url = diskCacheURL.appendingPathComponent("\(key).cache")
            let data = try Data(contentsOf: url)
            return try JSONDecoder().decode(Value.self, from: data)
        } catch {
            return nil
        }
    }
}

// Usage example
let hybridCache = HybridCache<String, User>()

hybridCache.set("user:456", value: User(
    id: "456", 
    name: "Bob Wilson", 
    email: "[email protected]", 
    createdAt: Date()
))

if let user = hybridCache.get("user:456") {
    print("Retrieved user: \(user.name)")
}

Cache Statistics and Monitoring

import Cache

class MonitoredCache<Key: Hashable, Value> {
    private let cache = Cache<Key, Value>()
    private var hitCount = 0
    private var missCount = 0
    private var setCount = 0
    
    var hitRate: Double {
        let total = hitCount + missCount
        return total > 0 ? Double(hitCount) / Double(total) : 0
    }
    
    func set(_ key: Key, value: Value, ttl: TimeInterval? = nil) {
        cache.set(key, value: value, ttl: ttl)
        setCount += 1
    }
    
    func get(_ key: Key) -> Value? {
        if let value = cache.get(key) {
            hitCount += 1
            return value
        } else {
            missCount += 1
            return nil
        }
    }
    
    func statistics() -> (hits: Int, misses: Int, sets: Int, hitRate: Double) {
        return (hitCount, missCount, setCount, hitRate)
    }
    
    func resetStatistics() {
        hitCount = 0
        missCount = 0
        setCount = 0
    }
}

// Usage example
let monitoredCache = MonitoredCache<String, String>()

monitoredCache.set("key1", value: "value1")
monitoredCache.set("key2", value: "value2")

_ = monitoredCache.get("key1") // Hit
_ = monitoredCache.get("key3") // Miss

let stats = monitoredCache.statistics()
print("Hit rate: \(stats.hitRate)") // 0.5 (50%)