import Combine import SwiftUI @MainActor final class ImageCacheManager { // MARK: - Singleton static let shared = ImageCacheManager() // MARK: - Private Properties private let cache = URLCache( memoryCapacity: 100 * 1_024 * 1_024, // 100 MB diskCapacity: 500 * 1_024 * 1_024, // 500 MB directory: FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first? .appendingPathComponent("SoraImageCache", isDirectory: true) ) private var cancellables = Set() private let downloadQueue = OperationQueue() private var preloadingURLs = Set() // MARK: - Initialisation private init() { downloadQueue.maxConcurrentOperationCount = 5 downloadQueue.qualityOfService = .utility } // MARK: - Public Methods func preloadImages(_ urls: [URL]) { let newURLs = urls.filter { !preloadingURLs.contains($0) } for url in newURLs { preloadingURLs.insert(url) downloadQueue.addOperation { let cancellable = URLSession.shared.dataTaskPublisher(for: url) .map { CachedURLResponse(response: $0.response, data: $0.data) } .sink( receiveCompletion: { _ in DispatchQueue.main.async { [weak self] in self?.preloadingURLs.remove(url) } }, receiveValue: { [weak self] cachedResponse in self?.cache.storeCachedResponse(cachedResponse, for: URLRequest(url: url)) } ) DispatchQueue.main.async { [weak self] in self?.cancellables.insert(cancellable) } } } } func clearCache() { cache.removeAllCachedResponses() cancellables.removeAll() preloadingURLs.removeAll() } func getCachedResponse(for url: URL) -> CachedURLResponse? { cache.cachedResponse(for: URLRequest(url: url)) } }