Cache (Swift)

SwiftライブラリキャッシュiOSmacOSSPMメモリディスク

ライブラリ

Cache (Swift)

概要

CacheはSwift Package Managerに対応したモダンなSwiftキャッシュライブラリです。メモリとディスクの両方に対応し、時間制限や容量制限をサポートするiOS・macOS向けのキャッシュソリューションです。

詳細

Cache(キャッシュ)は、Swift Package Managerの普及により注目度が上昇している、モダンなSwift開発向けのキャッシュライブラリです。メモリとディスクの両方でのキャッシュをサポートし、TTL(Time To Live)による時間制限と容量制限機能を提供します。iOS 9/macOS 10.11以降で動作し、Codableプロトコルに準拠したカスタム型やプリミティブ型(String、Intなど)のキャッシュが可能です。Swift Package Manager(SPM)エコシステムとの統合により、モダンなSwift開発ワークフローに自然に組み込むことができ、0xLeifによって開発されています。NSCacheやHanekeSwiftといった他のSwiftキャッシュライブラリと比較して、SPMネイティブサポートとモダンなSwift言語機能を活用した設計が特徴です。

メリット・デメリット

メリット

  • Swift Package Manager対応: モダンなSwift開発環境との完全統合
  • メモリ・ディスク両対応: 柔軟なキャッシュ戦略の実装
  • Codable対応: 型安全なキャッシュ操作とSwiftネイティブなシリアライゼーション
  • 時間・容量制限: TTLと容量制限による自動管理
  • iOS/macOS対応: Apple プラットフォーム全般での使用
  • シンプルAPI: 直感的で使いやすいインターフェース
  • 軽量設計: 最小限の依存関係とメモリフットプリント

デメリット

  • Apple プラットフォーム限定: iOS・macOS以外での使用不可
  • 新しいライブラリ: 実績が他の成熟したライブラリより少ない
  • 機能制限: 高度なキャッシュ機能は他の専用ライブラリに劣る
  • コミュニティサイズ: NSCacheなどの標準ライブラリと比較してコミュニティが小さい
  • ドキュメント: 包括的なドキュメントが限定的

主要リンク

書き方の例

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"]
        )
    ]
)

基本的なキャッシュ使用

import Cache

// Cacheインスタンスを作成
let cache = Cache<String, String>()

// 値を設定
cache.set("user:123", value: "John Doe")

// 値を取得
if let userName = cache.get("user:123") {
    print("User name: \(userName)")
}

// 値を削除
cache.remove("user:123")

// キャッシュクリア
cache.clear()

TTL(Time To Live)付きキャッシュ

import Cache

// TTL対応キャッシュ
let cache = Cache<String, String>(defaultTTL: 300) // 5分

// TTLを指定して値を設定
cache.set("temporary_data", value: "This will expire", ttl: 60) // 1分で期限切れ

// 期限切れチェック
if cache.isExpired("temporary_data") {
    print("Data has expired")
} else {
    let data = cache.get("temporary_data")
    print("Data: \(data ?? "nil")")
}

Codable型のキャッシュ

import Cache

// Codable準拠の構造体
struct User: Codable {
    let id: String
    let name: String
    let email: String
    let createdAt: Date
}

// Userオブジェクト用キャッシュ
let userCache = Cache<String, User>()

// ユーザーオブジェクトをキャッシュ
let user = User(
    id: "123",
    name: "Jane Smith",
    email: "[email protected]",
    createdAt: Date()
)

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

// ユーザーオブジェクトを取得
if let cachedUser = userCache.get("user:123") {
    print("Cached user: \(cachedUser.name)")
}

キャッシュサイズ制限

import Cache

// 最大容量制限付きキャッシュ
let cache = Cache<String, Data>(maxSize: 100) // 最大100アイテム

// 容量超過時の動作確認
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 (最大容量)
print("First item exists: \(cache.get("item:1") != nil)") // false (削除済み)
print("Last item exists: \(cache.get("item:150") != nil)") // true

非同期キャッシュ操作

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()
            }
        }
    }
}

// 使用例
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)")
    }
}

メモリ・ディスク複合キャッシュ

import Cache
import Foundation

class HybridCache<Key: Hashable & Codable, Value: Codable> {
    private let memoryCache = Cache<Key, Value>(maxSize: 50) // メモリ最大50アイテム
    private let diskCacheURL: URL
    
    init(diskCacheDirectory: String = "HybridCache") {
        let documentsPath = FileManager.default.urls(for: .documentDirectory, 
                                                   in: .userDomainMask).first!
        self.diskCacheURL = documentsPath.appendingPathComponent(diskCacheDirectory)
        
        // ディレクトリ作成
        try? FileManager.default.createDirectory(at: diskCacheURL, 
                                                withIntermediateDirectories: true)
    }
    
    func set(_ key: Key, value: Value, ttl: TimeInterval? = nil) {
        // メモリキャッシュに保存
        memoryCache.set(key, value: value, ttl: ttl)
        
        // ディスクキャッシュに保存
        saveToDisk(key: key, value: value)
    }
    
    func get(_ key: Key) -> Value? {
        // まずメモリから試行
        if let value = memoryCache.get(key) {
            return value
        }
        
        // ディスクから取得してメモリに復元
        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
        }
    }
}

// 使用例
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)")
}

キャッシュ統計とモニタリング

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
    }
}

// 使用例
let monitoredCache = MonitoredCache<String, String>()

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

_ = monitoredCache.get("key1") // ヒット
_ = monitoredCache.get("key3") // ミス

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