import Foundation import SwiftUI #if canImport(UIKit) import UIKit public typealias PlatformImage = UIImage #else import AppKit public typealias PlatformImage = NSImage #endif /// Lädt Bilder vom Server — einmal, und behält sie. /// /// `AsyncImage` konnte das nicht: es lädt bei **jedem** Erscheinen neu, kennt /// keinen Zwischenspeicher und legt für jedes Bild eine eigene Anfrage an. Eine /// Startseite zeigt leicht sechzig Kacheln; dazu ein Aufmacher, der ein großes /// Bild anfordert, das der Server erst umrechnen muss. Mit sechs gleichzeitigen /// Verbindungen je Gegenstelle — der Voreinstellung — stauen sich die übrigen /// dahinter, und wer wegscrollt und zurückkommt, stellt sich hinten wieder an. /// /// Auf dem iPad fiel das nicht auf, auf dem Mac schon: dort blieben der /// Aufmacher und die Hälfte der Plakate grau. /// /// Zwei Speicher übereinander: die entpackten Bilder im Arbeitsspeicher, die /// Rohdaten auf der Platte. Der zweite überlebt einen Neustart der App — /// dieselbe Startseite kostet dann keine einzige Anfrage mehr. public actor ArtworkLoader { public static let shared = ArtworkLoader() private let session: URLSession private let cache = NSCache() /// Läuft eine Anfrage schon, wird sie geteilt statt ein zweites Mal /// gestellt. Ohne das lädt eine Reihe, die dasselbe Plakat zweimal zeigt, /// es auch zweimal. private var inFlight: [URL: Task] = [:] init() { let configuration = URLSessionConfiguration.default configuration.urlCache = URLCache( memoryCapacity: 32 * 1_024 * 1_024, diskCapacity: 512 * 1_024 * 1_024 ) configuration.requestCachePolicy = .returnCacheDataElseLoad // Bilder sind klein und viele. Sechs gleichzeitige Verbindungen sind // für eine Seite mit sechzig Kacheln zu wenig. configuration.httpMaximumConnectionsPerHost = 12 configuration.timeoutIntervalForRequest = 30 session = URLSession(configuration: configuration) cache.countLimit = 400 } public func image(for url: URL) async -> PlatformImage? { if let cached = cache.object(forKey: url as NSURL) { return cached } if let running = inFlight[url] { return await running.value } let task = Task { [session] in guard let (data, response) = try? await session.data(from: url), let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode), let image = PlatformImage(data: data) else { return nil } return image } inFlight[url] = task let image = await task.value inFlight[url] = nil if let image { cache.setObject(image, forKey: url as NSURL) } return image } /// Das Bild, wenn es schon im Arbeitsspeicher liegt. /// /// Damit eine Kachel, die zum zweiten Mal erscheint, nicht erst kurz grau /// aufblitzt — das ist beim Scrollen der sichtbarste Unterschied. public func cached(_ url: URL) -> PlatformImage? { cache.object(forKey: url as NSURL) } } extension Image { /// Aus einem Plattformbild, ohne dass die Ansicht wissen muss, welches. init(platform image: PlatformImage) { #if canImport(UIKit) self.init(uiImage: image) #else self.init(nsImage: image) #endif } }