Bilder ohne Warten — und der Beweis, dass der Mac die Leiste selbst zeichnet

Der Schriftzug hing weiter nicht an der Animation, und der Zwischenspeicher war
nicht schuld: er lag im Actor, und ein `await` kostet immer mindestens einen
Durchlauf. Das neue Bild erschien deshalb im ersten Bild ohne Schriftzug und im
zweiten mit — er sprang also getrennt herein, obwohl er längst da war.

Der Speicher liegt jetzt außerhalb des Actors und lässt sich beim Zeichnen
lesen. Das gilt für alle Bilder: auch eine zurückscrollende Kachel blitzt nicht
mehr kurz grau auf.

Zur Seitenleiste auf dem Mac, gemessen statt vermutet: `automatic` löst sich
laut Apples eigener Beschreibung auf iOS zu `overlap` auf (die Leiste schwebt
über dem Inhalt) und auf macOS zu `tile` (sie schiebt ihn zur Seite und klebt
am Fensterrand). Der Griff dorthin sitzt richtig — derselbe Weg hat die Leiste
auf Kommando ganz ausgeblendet —, aber `overlap` wird auf dem Mac
stillschweigend verworfen. Die Bitte bleibt trotzdem stehen; sie kostet nichts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-13 17:40:46 +02:00
co-authored by Claude Opus 5
parent e5e01838f7
commit e7de8a9055
7 changed files with 183 additions and 19 deletions
@@ -27,7 +27,16 @@ public actor ArtworkLoader {
public static let shared = ArtworkLoader()
private let session: URLSession
private let cache = NSCache<NSURL, PlatformImage>()
/// Der Speicher liegt **außerhalb** des Actors, damit er sich ohne
/// `await` lesen lässt.
///
/// Das klingt nach einer Kleinigkeit und ist der ganze Punkt: ein `await`
/// kostet immer mindestens einen Durchlauf, und genau der war beim
/// Überblenden des Aufmachers zu sehen. Das neue Bild erschien im ersten
/// Bild noch ohne Schriftzug, im zweiten mit — der Schriftzug sprang also
/// getrennt herein, obwohl er längst da war. Wer das Bild schon hat, soll
/// es im selben Augenblick zeigen können.
private let cache = ImageMemoryCache()
/// 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.
@@ -49,8 +58,15 @@ public actor ArtworkLoader {
cache.countLimit = 400
}
/// Das Bild, wenn es schon im Arbeitsspeicher liegt — ohne Warten.
///
/// `nonisolated`, damit eine Ansicht es beim Zeichnen aufrufen kann.
public nonisolated static func cachedImage(for url: URL) -> PlatformImage? {
shared.cache.image(for: url)
}
public func image(for url: URL) async -> PlatformImage? {
if let cached = cache.object(forKey: url as NSURL) { return cached }
if let cached = cache.image(for: url) { return cached }
if let running = inFlight[url] { return await running.value }
let task = Task<PlatformImage?, Never> { [session] in
@@ -65,16 +81,33 @@ public actor ArtworkLoader {
let image = await task.value
inFlight[url] = nil
if let image { cache.setObject(image, forKey: url as NSURL) }
if let image { cache.store(image, for: url) }
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)
}
/// Ein Bildspeicher, der sich von überall lesen lässt.
///
/// `NSCache` ist laut Apples Dokumentation von sich aus für den Zugriff aus
/// mehreren Strängen gebaut — der Compiler weiß das nur nicht. Deshalb hier
/// von Hand versichert statt den Speicher in einen Actor zu sperren, was ihn
/// genau um die Eigenschaft brächte, für die er hier steht: ohne Warten
/// lesbar zu sein.
private final class ImageMemoryCache: @unchecked Sendable {
private let storage = NSCache<NSURL, PlatformImage>()
var countLimit: Int {
get { storage.countLimit }
set { storage.countLimit = newValue }
}
func image(for url: URL) -> PlatformImage? {
storage.object(forKey: url as NSURL)
}
func store(_ image: PlatformImage, for url: URL) {
storage.setObject(image, forKey: url as NSURL)
}
}