Skip to content

Instantly share code, notes, and snippets.

@amitpdev
Created April 4, 2025 15:40
Show Gist options
  • Save amitpdev/95ba886f3490bfa7b6bcd74fd15c4172 to your computer and use it in GitHub Desktop.
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.
//
// 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()
}
}
@amitpdev
Copy link
Author

amitpdev commented Apr 4, 2025

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment