Absturz beim Laden des Hintergrundbilds — und derselbe Fehler in der Ablage
Was in einer View steht, ist an den Hauptthread gebunden: die Konformität zu `View` vererbt das, auch an statische Methoden, die damit nichts zu tun haben. Das Laden lief absichtlich nebenher — und lief damit in die Isolationsprüfung, die das Programm anhält. Der Compiler hat genau das gemeldet: warning: call to main actor-isolated static method 'systemWallpaper()' in a synchronous nonisolated context note: main actor isolation inferred from conformance to protocol 'View' Eine Warnung, keine Fehlermeldung. Ich hatte den Bau nur nach `error:` durchsucht und sie deshalb nicht gesehen. Das Laden liegt jetzt in einem eigenen Typ neben der View, wo es die Bindung nicht gibt. Dieselbe Suche über den ganzen Bau hat einen zweiten Fall gefunden: `FileDropView.stage` wird aus dem Abschlussblock von `loadFileRepresentation` gerufen, also von irgendeinem Thread, gehört aber zu einem `ViewModifier`. Das hätte jeden Dateiabwurf auf die Ablage angehalten. Dazu die Verbesserung, die beim letzten Mal wegen eines falschen Arbeitsverzeichnisses nicht angekommen ist: gesucht wird zuerst unter den Originalen und erst danach in `.thumbnails`. Die kleinen Bilder messen 214 Punkte in der Breite und sind auf einem Retina-Schirm sichtbar unscharf. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -131,7 +131,7 @@ private struct TriggerPreview: View {
|
|||||||
// `NSWorkspace` gehören dorthin —, das Bild daneben.
|
// `NSWorkspace` gehören dorthin —, das Bild daneben.
|
||||||
let own = Self.ownWallpaperURL()
|
let own = Self.ownWallpaperURL()
|
||||||
let loaded = await Task.detached(priority: .userInitiated) {
|
let loaded = await Task.detached(priority: .userInitiated) {
|
||||||
Wallpaper(image: own.flatMap { Self.thumbnail(of: $0) } ?? Self.systemWallpaper())
|
Wallpaper(image: WallpaperLoader.load(own: own))
|
||||||
}.value
|
}.value
|
||||||
Self.cache = loaded
|
Self.cache = loaded
|
||||||
wallpaper = loaded.image
|
wallpaper = loaded.image
|
||||||
@@ -172,38 +172,63 @@ private struct TriggerPreview: View {
|
|||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Lädt das Hintergrundbild der Vorschau.
|
||||||
|
///
|
||||||
|
/// **Ein eigener Typ, kein Teil der View.** Was in einer `View` steht, ist an
|
||||||
|
/// den Hauptthread gebunden — die Konformität vererbt das, auch an statische
|
||||||
|
/// Methoden, die damit gar nichts zu tun haben. Aufgerufen wurde es trotzdem
|
||||||
|
/// nebenher; der Compiler hat das nur angemahnt, und zur Laufzeit hat die
|
||||||
|
/// Prüfung die App angehalten. Hier draußen gibt es die Bindung nicht.
|
||||||
|
enum WallpaperLoader {
|
||||||
|
|
||||||
|
static func load(own: URL?) -> NSImage? {
|
||||||
|
own.flatMap(thumbnail(of:)) ?? systemWallpaper()
|
||||||
|
}
|
||||||
|
|
||||||
/// Ein Standardhintergrund von macOS.
|
/// Ein Standardhintergrund von macOS.
|
||||||
///
|
///
|
||||||
/// Die Vorschaubilder aus `.thumbnails` statt der Originale: dieselben
|
/// Sie liegen auf jedem Mac und werden nur gelesen, nicht mitgeliefert.
|
||||||
/// Motive in ein paar Dutzend Kilobyte statt in zwanzig Megabyte. Sie
|
|
||||||
/// liegen auf jedem Mac und werden nur gelesen, nicht mitgeliefert.
|
|
||||||
///
|
///
|
||||||
/// Die Reihenfolge geht von neu nach alt und endet bei „irgendein Foto":
|
/// Die Reihenfolge geht von neu nach alt und endet bei „irgendein Foto":
|
||||||
/// welche Bilder installiert sind, hängt an der macOS-Version.
|
/// welche Bilder installiert sind, hängt an der macOS-Version.
|
||||||
private static func systemWallpaper() -> NSImage? {
|
static func systemWallpaper() -> NSImage? {
|
||||||
let directory = "/System/Library/Desktop Pictures"
|
let directory = "/System/Library/Desktop Pictures"
|
||||||
let preferred = ["Tahoe", "Sequoia", "Sonoma", "Ventura", "Monterey",
|
let preferred = ["Tahoe", "Sequoia", "Sonoma", "Ventura", "Monterey",
|
||||||
"Big Sur Mountains", "Catalina Coast"]
|
"Big Sur Mountains", "Catalina Coast"]
|
||||||
|
|
||||||
let thumbnails = directory + "/.thumbnails"
|
// Zuerst die Originale, dann die kleinen Bilder aus `.thumbnails`.
|
||||||
let available = (try? FileManager.default.contentsOfDirectory(atPath: thumbnails)) ?? []
|
//
|
||||||
|
// Die Originale sind zwanzig Megabyte groß, werden aber nie ganz
|
||||||
for name in preferred {
|
// gelesen — `CGImageSource` dekodiert gleich in der gebrauchten Größe.
|
||||||
// „Sonoma.heic" vor „Sonoma Dark.heic": das schlichte Motiv ist das
|
// Die Vorschaubilder messen 214 Punkte in der Breite und wären auf
|
||||||
// bekannte, die Varianten sind Abwandlungen davon.
|
// einem Retina-Schirm sichtbar unscharf; sie sind die Reserve für die
|
||||||
let matches = available
|
// Motive, die macOS erst bei Bedarf nachlädt.
|
||||||
.filter { $0.hasPrefix(name) && $0.hasSuffix(".heic") }
|
for folder in [directory, directory + "/.thumbnails"] {
|
||||||
.sorted { $0.count < $1.count }
|
let available = (try? FileManager.default.contentsOfDirectory(atPath: folder)) ?? []
|
||||||
if let match = matches.first,
|
for name in preferred {
|
||||||
let image = thumbnail(of: URL(fileURLWithPath: thumbnails + "/" + match)) {
|
// „Sonoma.heic" vor „Sonoma Dark.heic": das schlichte Motiv ist
|
||||||
return image
|
// das bekannte, die Varianten sind Abwandlungen davon.
|
||||||
|
let matches = available
|
||||||
|
.filter { $0.hasPrefix(name) && $0.hasSuffix(".heic") }
|
||||||
|
.sorted { $0.count < $1.count }
|
||||||
|
if let match = matches.first,
|
||||||
|
let image = thumbnail(of: URL(fileURLWithPath: folder + "/" + match)) {
|
||||||
|
return image
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nichts Bekanntes dabei: dann das erste Bild, das sich lesen lässt.
|
// Nichts Bekanntes dabei: dann das erste Bild, das sich lesen lässt.
|
||||||
for name in available.sorted() where name.hasSuffix(".heic") {
|
for folder in [directory, directory + "/.thumbnails"] {
|
||||||
if let image = thumbnail(of: URL(fileURLWithPath: thumbnails + "/" + name)) {
|
let available = ((try? FileManager.default.contentsOfDirectory(atPath: folder)) ?? [])
|
||||||
return image
|
.filter { $0.hasSuffix(".heic") }.sorted()
|
||||||
|
for name in available {
|
||||||
|
if let image = thumbnail(of: URL(fileURLWithPath: folder + "/" + name)) {
|
||||||
|
return image
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -215,7 +240,7 @@ private struct TriggerPreview: View {
|
|||||||
/// paar hundert Bildpunkte breit, das Original hat sechstausend. Und ein
|
/// paar hundert Bildpunkte breit, das Original hat sechstausend. Und ein
|
||||||
/// Hintergrundbild kann mehrere Bilder enthalten — hell und dunkel im
|
/// Hintergrundbild kann mehrere Bilder enthalten — hell und dunkel im
|
||||||
/// selben HEIC —, von denen hier nur das erste gebraucht wird.
|
/// selben HEIC —, von denen hier nur das erste gebraucht wird.
|
||||||
private static func thumbnail(of url: URL) -> NSImage? {
|
static func thumbnail(of url: URL) -> NSImage? {
|
||||||
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else { return nil }
|
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else { return nil }
|
||||||
let options: [CFString: Any] = [
|
let options: [CFString: Any] = [
|
||||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||||
|
|||||||
@@ -50,7 +50,14 @@ public struct FileDropModifier: ViewModifier {
|
|||||||
|
|
||||||
/// Rettet die Datei aus dem flüchtigen Verzeichnis in einen eigenen
|
/// Rettet die Datei aus dem flüchtigen Verzeichnis in einen eigenen
|
||||||
/// Zwischenordner.
|
/// Zwischenordner.
|
||||||
private static func stage(_ url: URL) -> URL? {
|
///
|
||||||
|
/// `nonisolated`, und das ist keine Formalie: der Block oben läuft auf
|
||||||
|
/// irgendeinem Thread, diese Methode gehört aber zu einem `ViewModifier`
|
||||||
|
/// und ist damit an den Hauptthread gebunden — die Bindung kommt über die
|
||||||
|
/// Konformität, nicht über den Inhalt. Der Compiler mahnt das nur an, zur
|
||||||
|
/// Laufzeit hält die Prüfung das Programm an. Hier ist ohnehin nichts
|
||||||
|
/// dabei, was den Hauptthread bräuchte.
|
||||||
|
nonisolated private static func stage(_ url: URL) -> URL? {
|
||||||
let directory = URL(fileURLWithPath: NSTemporaryDirectory())
|
let directory = URL(fileURLWithPath: NSTemporaryDirectory())
|
||||||
.appendingPathComponent("Onyx-Drop-\(UUID().uuidString)", isDirectory: true)
|
.appendingPathComponent("Onyx-Drop-\(UUID().uuidString)", isDirectory: true)
|
||||||
do {
|
do {
|
||||||
|
|||||||
Reference in New Issue
Block a user