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, 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, perform: @escaping ([URL]) -> Void) -> some View { modifier(FileDropModifier(isTargeted: isTargeted, onFiles: perform)) } }