import SwiftUI /// Ein Bild vom Server mit Lade- und Fehlerzustand. /// /// Der Austausch, den die frühere Notiz hier angekündigt hat, ist vollzogen: /// statt `AsyncImage` lädt `ArtworkLoader`. Der Grund stand schon da — eine /// Startseite zeigt dutzende Bilder gleichzeitig, und `AsyncImage` lädt sie bei /// jedem Erscheinen neu. Auf dem Mac wurde daraus ein sichtbarer Fehler: der /// Aufmacher und die Hälfte der Plakate blieben grau. public struct FlixyArtwork: View { private let url: URL? private let aspectRatio: CGFloat private let fallbackSymbol: String private let contentMode: ContentMode /// - Parameter contentMode: `.fill` füllt die Fläche und schneidet ab — /// richtig für Plakate und Szenenbilder, die ohnehin formatfüllend /// gedacht sind. `.fit` zeigt das ganze Bild. Das braucht alles, was /// kein festes Seitenverhältnis hat: **Sendersymbole** sind mal breit, /// mal schmal, und formatfüllend in einem Quadrat fehlten links und /// rechts genau die Teile, an denen man den Sender erkennt. public init( url: URL?, aspectRatio: CGFloat, fallbackSymbol: String = "film", contentMode: ContentMode = .fill ) { self.url = url self.aspectRatio = aspectRatio self.fallbackSymbol = fallbackSymbol self.contentMode = contentMode } @State private var loaded: PlatformImage? @State private var hasFailed = false public var body: some View { Rectangle() .fill(Color.flixyPlaceholder) .aspectRatio(aspectRatio, contentMode: .fit) .overlay { content } .clipped() .task(id: url) { await load() } } /// Das Bild, das jetzt gezeigt werden kann. /// /// Der Blick in den Speicher steht **hier**, nicht in `load()`: eine /// Kachel, die zum zweiten Mal erscheint, zeigt ihr Bild damit schon im /// ersten Bild und blitzt nicht kurz grau auf. Über ein `await` ginge das /// nicht — das kostet immer einen Durchlauf. private var available: PlatformImage? { if let loaded { return loaded } guard let url else { return nil } return ArtworkLoader.cachedImage(for: url) } @ViewBuilder private var content: some View { if let loaded = available { switch contentMode { case .fill: Image(platform: loaded).resizable().scaledToFill() case .fit: Image(platform: loaded).resizable().scaledToFit() } } else if hasFailed { // Kein Fehlertext: bei einer Reihe aus zwanzig Kacheln wäre das // mehr Lärm als Hilfe. Das Symbol sagt genug. Image(systemName: fallbackSymbol) .font(.title2) .foregroundStyle(Color.flixyTextMuted) } else { ShimmerPlaceholder() } } private func load() async { guard let url else { hasFailed = false loaded = nil return } // Was schon da ist, zeigt `available` bereits — hier bleibt nur das // Nachladen. if ArtworkLoader.cachedImage(for: url) != nil { hasFailed = false return } loaded = nil hasFailed = false let image = await ArtworkLoader.shared.image(for: url) guard !Task.isCancelled else { return } withAnimation(.easeOut(duration: 0.2)) { loaded = image hasFailed = image == nil } } }