// // ImageCache.swift // Reflect // // In-memory image cache for thumbnail images to improve scrolling performance. // import UIKit import SwiftUI /// Thread-safe in-memory image cache final class ImageCache { static let shared = ImageCache() private let cache = NSCache() private let queue = DispatchQueue(label: "com.88oakapps.reflect.imagecache", qos: .userInitiated) private var memoryWarningToken: NSObjectProtocol? private init() { // Configure cache limits cache.countLimit = 100 // Max 100 images cache.totalCostLimit = 50 * 1024 * 1024 // 50 MB max // Clear cache on memory warning memoryWarningToken = NotificationCenter.default.addObserver( forName: UIApplication.didReceiveMemoryWarningNotification, object: nil, queue: .main ) { [weak self] _ in self?.clearCache() } } deinit { if let token = memoryWarningToken { NotificationCenter.default.removeObserver(token) } } // MARK: - Public API /// Get image from cache func image(forKey key: String) -> UIImage? { queue.sync { cache.object(forKey: key as NSString) } } /// Store image in cache func setImage(_ image: UIImage, forKey key: String) { queue.async { [weak self] in let cost = Int(image.size.width * image.size.height * 4) // Approximate memory cost self?.cache.setObject(image, forKey: key as NSString, cost: cost) } } /// Remove image from cache func removeImage(forKey key: String) { queue.async { [weak self] in self?.cache.removeObject(forKey: key as NSString) } } /// Clear all cached images func clearCache() { queue.async { [weak self] in self?.cache.removeAllObjects() } } // MARK: - Convenience Methods for Photo IDs /// Get cached image for photo ID func image(forPhotoID id: UUID) -> UIImage? { image(forKey: id.uuidString) } /// Store image for photo ID func setImage(_ image: UIImage, forPhotoID id: UUID) { setImage(image, forKey: id.uuidString) } /// Remove cached image for photo ID func removeImage(forPhotoID id: UUID) { removeImage(forKey: id.uuidString) } } // MARK: - Cached Photo Loading extension PhotoManager { /// Load thumbnail with caching func loadCachedThumbnail(id: UUID) -> UIImage? { // Check cache first if let cached = ImageCache.shared.image(forPhotoID: id) { return cached } // Load from disk guard let image = loadThumbnail(id: id) else { return nil } // Cache for future use ImageCache.shared.setImage(image, forPhotoID: id) return image } /// Load full image with caching func loadCachedPhoto(id: UUID) -> UIImage? { let cacheKey = "\(id.uuidString)-full" // Check cache first if let cached = ImageCache.shared.image(forKey: cacheKey) { return cached } // Load from disk guard let image = loadPhoto(id: id) else { return nil } // Cache for future use ImageCache.shared.setImage(image, forKey: cacheKey) return image } } // MARK: - SwiftUI Cached Image View struct CachedAsyncImage: View { let photoID: UUID? let useThumbnail: Bool @State private var image: UIImage? @State private var isLoading = false init(photoID: UUID?, useThumbnail: Bool = true) { self.photoID = photoID self.useThumbnail = useThumbnail } var body: some View { Group { if let image = image { Image(uiImage: image) .resizable() .aspectRatio(contentMode: .fill) } else if isLoading { ProgressView() } else { Color.gray.opacity(0.2) } } .onAppear { loadImage() } } @MainActor private func loadImage() { guard let id = photoID else { return } isLoading = true Task { let loadedImage = await Task.detached(priority: .userInitiated) { if useThumbnail { return await PhotoManager.shared.loadCachedThumbnail(id: id) } else { return await PhotoManager.shared.loadCachedPhoto(id: id) } }.value await MainActor.run { self.image = loadedImage self.isLoading = false } } } }