@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], domain: String, sendUserAgent: Bool, customUserAgent: String ) { let newURLs = urls.filter { url in let request = BooruRequestConfiguration.request( url: url, domain: domain, sendUserAgent: sendUserAgent, customUserAgent: customUserAgent, accept: "image/*" ) return !preloadingURLs.contains(url) && cache.cachedResponse(for: request) == nil } for url in newURLs { preloadingURLs.insert(url) downloadQueue.addOperation { [weak self] in guard let self else { return } let completionSignal = DispatchSemaphore(value: 0) let request = BooruRequestConfiguration.request( url: url, domain: domain, sendUserAgent: sendUserAgent, customUserAgent: customUserAgent, accept: "image/*" ) URLSession.shared.dataTask(with: request) { 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: request ) } } .resume() completionSignal.wait() } } } func clearCache() { cache.removeAllCachedResponses() preloadingURLs.removeAll() } func storeCachedResponse(data: Data, response: URLResponse, for request: URLRequest) { cache.storeCachedResponse( CachedURLResponse(response: response, data: data), for: request ) } func getCachedResponse(for request: URLRequest) -> CachedURLResponse? { cache.cachedResponse(for: request) } func loadImageData( for url: URL, domain: String, sendUserAgent: Bool, customUserAgent: String ) async -> Data? { let request = BooruRequestConfiguration.request( url: url, domain: domain, sendUserAgent: sendUserAgent, customUserAgent: customUserAgent, accept: "image/*" ) if let cachedResponse = getCachedResponse(for: request) { return cachedResponse.data } do { let (data, response) = try await URLSession.shared.data(for: request) storeCachedResponse( data: data, response: response, for: request ) return data } catch { return nil } } private func handleMemoryPressure() { cache.removeAllCachedResponses() downloadQueue.cancelAllOperations() preloadingURLs.removeAll() } deinit { if let observer = memoryWarningObserver { NotificationCenter.default.removeObserver(observer) } } }