onmyway133 / blog

🍁 What you don't know is what you haven't learned
https://onmyway133.com/
MIT License
679 stars 33 forks source link

How to drag multiple files in SwiftUI on Mac #951

Open onmyway133 opened 1 year ago

onmyway133 commented 1 year ago

Create a custom NSView that handles mouseDragged to beginDraggingSession

struct DraggingSourceViewWrapper: NSViewRepresentable {
    let fileUrls: [URL]
    let onEnd: () -> Void

    func makeNSView(context: Context) -> DraggingSourceView {
        let view = DraggingSourceView(fileUrls: fileUrls)
        view.onEnd = onEnd
        return view
    }

    func updateNSView(_ view: DraggingSourceView, context: Context) {
        view.onEnd = onEnd
    }
}

final class DraggingSourceView: NSView {
    let fileUrls: [URL]
    var onEnd: (() -> Void)?

    init(fileUrls: [URL]) {
        self.fileUrls = fileUrls
        super.init(frame: .zero)
    }

    required init?(coder: NSCoder) {
        fatalError()
    }

    override func mouseDown(with event: NSEvent) {
        // NOTE: Needed for mouseDragged to work
    }

    override func mouseDragged(with event: NSEvent) {
        let draggingItems = fileUrls.map { url in
            let pasteboardItem = NSPasteboardItem()
            pasteboardItem.setString(url.absoluteString, forType: .string)
            pasteboardItem.setDataProvider(self, forTypes: [.fileURL])

            let image = NSWorkspace.shared.icon(forFile: url.path)

            let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem)
            draggingItem.setDraggingFrame(bounds, contents: image)
            return draggingItem
        }

        beginDraggingSession(with: draggingItems, event: event, source: self)
    }
}

extension DraggingSourceView: NSDraggingSource {
    func draggingSession(
        _ session: NSDraggingSession,
        sourceOperationMaskFor context: NSDraggingContext
    ) -> NSDragOperation {
        switch context {
        case .outsideApplication:
            return .move

        default:
            return .private
        }
    }

    func draggingSession(_ session: NSDraggingSession, endedAt screenPoint: NSPoint, operation: NSDragOperation) {
        onEnd?()
    }
}

extension DraggingSourceView: NSPasteboardItemDataProvider {
    nonisolated func pasteboard(
        _ pasteboard: NSPasteboard?,
        item: NSPasteboardItem,
        provideDataForType type: NSPasteboard.PasteboardType
    ) {
        guard
            let string = item.string(forType: .string),
            let url = URL(string: string)
        else { return }

        if let url = BookmarkService.shared.resolve(url: url) {
            url.access {
                let data = url.absoluteString.data(using: .utf8) ?? Data()
                item.setData(data, forType: .fileURL)
            }
        }
    }
}