@preconcurrency 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 let downloadQueue = OperationQueue() private var preloadingURLs = Set() private var memoryWarningObserver: NSObjectProtocol? // MARK: - Initialisation private init() { downloadQueue.maxConcurrentOperationCount = 3 downloadQueue.qualityOfService = .utility #if os(iOS) memoryWarningObserver = NotificationCenter.default.addObserver( forName: UIApplication.didReceiveMemoryWarningNotification, object: nil, queue: .main ) { [weak self] _ in Task { @MainActor in self?.handleMemoryPressure() } } #endif } // MARK: - Public Methods func preloadImages(_ urls: [URL]) { let newURLs = urls.filter { url in !preloadingURLs.contains(url) && cache.cachedResponse(for: URLRequest(url: url)) == nil } for url in newURLs { preloadingURLs.insert(url) downloadQueue.addOperation { [weak self] in guard let self else { return } let completionSignal = DispatchSemaphore(value: 0) URLSession.shared.dataTask(with: url) { data, response, _ in Task { @MainActor [weak self] in guard let self else { completionSignal.signal() return } defer { preloadingURLs.remove(url) completionSignal.signal() } guard let data, let response else { return } storeCachedResponse( data: data, response: response, for: url ) } } .resume() completionSignal.wait() } } } func clearCache() { cache.removeAllCachedResponses() preloadingURLs.removeAll() } func storeCachedResponse(data: Data, response: URLResponse, for url: URL) { cache.storeCachedResponse( CachedURLResponse(response: response, data: data), for: URLRequest(url: url) ) } func getCachedResponse(for url: URL) -> CachedURLResponse? { cache.cachedResponse(for: URLRequest(url: url)) } func loadImageData(for url: URL) async -> Data? { if let cachedResponse = getCachedResponse(for: url) { return cachedResponse.data } do { let (data, response) = try await URLSession.shared.data(from: url) storeCachedResponse( data: data, response: response, for: url ) return data } catch { return nil } } private func handleMemoryPressure() { cache.removeAllCachedResponses() downloadQueue.cancelAllOperations() preloadingURLs.removeAll() } deinit { if let observer = memoryWarningObserver { NotificationCenter.default.removeObserver(observer) } } }