Files
onyx/Packages/OnyxKit/Sources/ShelfProvider/FileDropView.swift
Scarriffle a2be84f4d5 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>
2026-08-13 23:43:22 +02:00

87 lines
3.9 KiB
Swift

import SwiftUI
import UniformTypeIdentifiers
import OSLog
private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "Shelf")
/// Nimmt gezogene Dateien an auch die, die es noch gar nicht gibt.
///
/// Mail, WhatsApp und die meisten Chat-Programme ziehen Anhänge nicht als
/// fertige Datei heraus, sondern als **Versprechen**: die Datei entsteht erst,
/// wenn das Ziel sagt, wohin. Genau diese Anhänge sind der Hauptgrund für eine
/// Ablage, also darf die Fläche nicht nur `public.file-url` annehmen.
///
/// `loadFileRepresentation` ist der Weg, der beides abdeckt: bei einer echten
/// Datei reicht es sie durch, bei einem Versprechen löst es dieses ein. Deshalb
/// `UTType.item` als angenommener Typ die Wurzel, der alles entspricht.
/// Was sich nicht als Datei materialisieren lässt, fällt hinten heraus.
public struct FileDropModifier: ViewModifier {
@Binding private var isTargeted: Bool
private let onFiles: ([URL]) -> Void
public init(isTargeted: Binding<Bool>, onFiles: @escaping ([URL]) -> Void) {
self._isTargeted = isTargeted
self.onFiles = onFiles
}
public func body(content: Content) -> some View {
content.onDrop(of: [.item], isTargeted: $isTargeted) { providers in
for provider in providers { receive(provider) }
return !providers.isEmpty
}
}
private func receive(_ provider: NSItemProvider) {
provider.loadFileRepresentation(forTypeIdentifier: UTType.item.identifier) { url, error in
if let error {
log.error("Ablage: Datei nicht erhalten: \(error.localizedDescription, privacy: .public)")
return
}
guard let url else { return }
// Die übergebene Datei existiert **nur innerhalb dieses Blocks**;
// danach räumt das System sie weg. Wer sie erst auf dem MainActor
// anfassen will, greift ins Leere also hier und sofort sichern.
guard let staged = Self.stage(url) else { return }
Task { @MainActor in onFiles([staged]) }
}
}
/// Rettet die Datei aus dem flüchtigen Verzeichnis in einen eigenen
/// Zwischenordner.
///
/// `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())
.appendingPathComponent("Onyx-Drop-\(UUID().uuidString)", isDirectory: true)
do {
try FileManager.default.createDirectory(at: directory,
withIntermediateDirectories: true)
let target = directory.appendingPathComponent(url.lastPathComponent)
// Verschieben, wenn möglich: bei einem Video von zwei Gigabyte ist
// der Unterschied zum Kopieren spürbar. Über Datenträgergrenzen
// hinweg scheitert es, dann wird kopiert.
do { try FileManager.default.moveItem(at: url, to: target) }
catch { try FileManager.default.copyItem(at: url, to: target) }
return target
} catch {
log.error("Ablage: Zwischenspeichern gescheitert: \(error.localizedDescription, privacy: .public)")
return nil
}
}
}
public extension View {
/// Macht die ganze Fläche zum Ziel für gezogene Dateien.
func onFileDrop(isTargeted: Binding<Bool>,
perform: @escaping ([URL]) -> Void) -> some View {
modifier(FileDropModifier(isTargeted: isTargeted, onFiles: perform))
}
}