Created
April 4, 2025 15:40
-
-
Save amitpdev/95ba886f3490bfa7b6bcd74fd15c4172 to your computer and use it in GitHub Desktop.
A simple and efficient cache handler for iOS that stores data in memory and on disk, with support for cache expiration and preloaded cache files.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// CacheHandler.swift | |
// Nester | |
// | |
// Created by Amit Palomo on 08/09/2024 | |
// | |
import Foundation | |
import CommonCrypto | |
enum CacheOption { | |
case memory | |
case disk | |
} | |
class CacheHandler { | |
static let shared = CacheHandler() | |
private let memoryCache = NSCache<NSString, AnyObject>() | |
private let fileManager = FileManager.default | |
private let cacheDirectory: URL | |
private let diskCacheExpiration: TimeInterval = 6 * 24 * 60 * 60 // 6 days | |
private init() { | |
// Create a directory for storing cached responses | |
if let cacheDir = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first { | |
cacheDirectory = cacheDir.appendingPathComponent("URLCache", isDirectory: true) | |
if !fileManager.fileExists(atPath: cacheDirectory.path) { | |
try? fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true, attributes: nil) | |
debugPrint("[CacheHandler] Created cache directory at \(cacheDirectory)") | |
} else { | |
debugPrint("[CacheHandler] Found cache directory at \(cacheDirectory)") | |
} | |
copyPreloadedCacheIfNeeded() | |
} else { | |
fatalError("[CacheHandler] Could not access cache directory.") | |
} | |
} | |
private func copyPreloadedCacheIfNeeded() { | |
guard let bundleCacheDirectory = Bundle.main.resourceURL?.appendingPathComponent("PreloadedCache") else { | |
debugPrint("[CacheHandler] Bundle cache directory URL is nil.") | |
return | |
} | |
var isDirectory: ObjCBool = false | |
if !fileManager.fileExists(atPath: bundleCacheDirectory.path, isDirectory: &isDirectory) || !isDirectory.boolValue { | |
debugPrint("[CacheHandler] Bundle cache directory does not exist or is not a directory.") | |
return | |
} | |
debugPrint("[CacheHandler] Bundle cache directory: \(bundleCacheDirectory.path)") | |
do { | |
let preloadedCacheFiles = try fileManager.contentsOfDirectory(at: bundleCacheDirectory, includingPropertiesForKeys: nil) | |
var filesCopiedCounter = 0 | |
for file in preloadedCacheFiles { | |
let destinationURL = cacheDirectory.appendingPathComponent(file.lastPathComponent) | |
if !fileManager.fileExists(atPath: destinationURL.path) { | |
try fileManager.copyItem(at: file, to: destinationURL) | |
// Update the modification date to current date | |
let currentDate = Date() | |
try fileManager.setAttributes([.modificationDate: currentDate], ofItemAtPath: destinationURL.path) | |
filesCopiedCounter += 1 | |
} | |
} | |
if filesCopiedCounter > 0 { | |
debugPrint("[CacheHandler] \(filesCopiedCounter) preloaded cache files copied from bundle.") | |
} | |
} catch { | |
debugPrint("[CacheHandler] Failed to copy preloaded cache files: \(error)") | |
} | |
} | |
// Retrieve cached response from memory or disk | |
func getCachedResponse(forKey key: String, cacheOptions: [CacheOption] = []) -> AnyObject? { | |
if cacheOptions.contains(.memory), | |
let cachedResponse = memoryCache.object(forKey: key as NSString) { | |
debugPrint("[CacheHandler] Return memory cached response for key: \(key)") | |
return cachedResponse | |
} else if cacheOptions.contains(.disk), | |
let diskResponse = loadResponseFromDisk(forKey: key) { | |
debugPrint("[CacheHandler] Return disk cached response for key: \(key)") | |
if cacheOptions.contains(.memory) { | |
memoryCache.setObject(diskResponse, forKey: key as NSString) | |
} | |
return diskResponse | |
} | |
return nil | |
} | |
// Cache response in memory and/or disk | |
func cacheResponse(_ response: AnyObject, forKey key: String, cacheOptions: [CacheOption] = []) { | |
debugPrint("[CacheHandler] Saving response to cache with key: \(key)") | |
if cacheOptions.contains(.memory) { | |
memoryCache.setObject(response, forKey: key as NSString) | |
} | |
if cacheOptions.contains(.disk) { | |
saveResponseToDisk(response, forKey: key) | |
} | |
} | |
// Save response to disk | |
private func saveResponseToDisk(_ response: AnyObject, forKey key: String) { | |
let fileURL = cacheDirectory.appendingPathComponent(key.sha256Hash) | |
let cacheData: [String: Any] = ["response": response] | |
if let data = try? JSONSerialization.data(withJSONObject: cacheData, options: []) { | |
try? data.write(to: fileURL, options: .atomic) | |
} | |
} | |
// Load response from disk and check expiration | |
private func loadResponseFromDisk(forKey key: String) -> AnyObject? { | |
let fileURL = cacheDirectory.appendingPathComponent(key.sha256Hash) | |
guard fileManager.fileExists(atPath: fileURL.path) else { | |
return nil | |
} | |
do { | |
let attributes = try fileManager.attributesOfItem(atPath: fileURL.path) | |
if let fileModificationDate = attributes[.modificationDate] as? Date { | |
let timeInterval = Date().timeIntervalSince(fileModificationDate) | |
if timeInterval <= diskCacheExpiration, | |
let data = try? Data(contentsOf: fileURL), | |
let cacheData = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], | |
let response = cacheData["response"] { | |
return response as AnyObject | |
} else { | |
try? fileManager.removeItem(at: fileURL) | |
} | |
} | |
} catch { | |
debugPrint("[CacheHandler] Error reading file attributes: \(error)") | |
} | |
return nil | |
} | |
} | |
extension String { | |
// Generate SHA256 hash | |
var sha256Hash: String { | |
guard let data = self.data(using: .utf8) else { return self } | |
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) | |
data.withUnsafeBytes { | |
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash) | |
} | |
return hash.map { String(format: "%02x", $0) }.joined() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
CacheHandler is a lightweight, singleton-based utility for caching data in-memory (NSCache) and on-disk (file-based) with optional preloaded cache support. It's designed to improve performance and responsiveness for apps that fetch large or expensive-to-compute resources, such as JSON responses or image data.
π§ Features
β In-memory caching using NSCache for fast access
πΎ Disk caching with expiration handling based on file modification date
π¦ Preloaded cache support for shipping cached data with the app bundle
π Filename safety using SHA256 hashing of keys
π Automatic promotion of disk-cached data to memory (when .memory is requested)
β³ Cache expiration configurable (default: 6 days for disk cache)
π Debug logs for visibility into cache operations
Use it to cache any AnyObject conforming data structures, like serialized JSON objects, dictionaries, etc.