Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI Drag and Drop files

I am trying to add a "Drag and Drop" gesture / function to my SwiftUI Mac application.

I want to drop files from my System/ Desktop into my Application. It is possbile in regular Swift, which I found. I am trying to do this in SwiftUI now.

I find a onDrop() function in SwiftUI for Views. However, it looks like that this is only for internal gestures inside my application. I want to drag files from outside.

In Swift you need to register your NSView, for dragged Types.

registerForDraggedTypes([kUTTypeFileURL,kUTTypeImage])

I thought of creating a NSViewRepresentable and wrap that into my SwiftUI view.

This is the code I came up with, however I can not call registerForDraggedTyped.

final class DragDropView: NSViewRepresentable {

    func makeNSView(context: NSViewRepresentableContext<DragDropView>) -> NSView {
        let view = NSView()

        view.registerForDraggedTypes([NSPasteboard.PasteboardType.pdf, NSPasteboard.PasteboardType.png])

        return view
    }

    func updateNSView(_ nsView: NSView, context: NSViewRepresentableContext<DragDropView>) {

    }

Is there a simpler solution for that in SwiftUI? I would love to use that onDrop() function, but this is not working for external files, is it?

like image 796
davidev Avatar asked Mar 24 '20 12:03

davidev


3 Answers

Here is a demo of drag & drop, tested with Xcode 11.4 / macOS 10.15.4.

Initial image is located on assets library, accepts drop (for simplicity only) as file url from Finder/Desktop (drop) and to TextEdit (drag), registers drag for TIFF representation.

struct TestImageDragDrop: View {
    @State var image = NSImage(named: "image")
    @State private var dragOver = false

    var body: some View {
        Image(nsImage: image ?? NSImage())
            .onDrop(of: ["public.file-url"], isTargeted: $dragOver) { providers -> Bool in
                providers.first?.loadDataRepresentation(forTypeIdentifier: "public.file-url", completionHandler: { (data, error) in
                    if let data = data, let path = NSString(data: data, encoding: 4), let url = URL(string: path as String) {
                        let image = NSImage(contentsOf: url)
                        DispatchQueue.main.async {
                            self.image = image
                        }
                    }
                })
                return true
            }
            .onDrag {
                let data = self.image?.tiffRepresentation
                let provider = NSItemProvider(item: data as NSSecureCoding?, typeIdentifier: kUTTypeTIFF as String)
                provider.previewImageHandler = { (handler, _, _) -> Void in
                    handler?(data as NSSecureCoding?, nil)
                }
                return provider
            }
            .border(dragOver ? Color.red : Color.clear)
    }
}
like image 142
Asperi Avatar answered Nov 01 '22 14:11

Asperi


As OP noted, this is only for a Mac OS application.

If you want to

  1. open from Finder
  2. drag from Finder
  3. AND drag from elsewhere (like a website or iMessage)

then try this (drag image from this view not included).

XCode 11+ / SwiftUI 1.0+ / Swift 5:

Required Extension for opening from Finder:

extension NSOpenPanel {
    
    static func openImage(completion: @escaping (_ result: Result<NSImage, Error>) -> ()) {
        let panel = NSOpenPanel()
        panel.allowsMultipleSelection = false
        panel.canChooseFiles = true
        panel.canChooseDirectories = false
        panel.allowedFileTypes = ["jpg", "jpeg", "png", "heic"]
        panel.canChooseFiles = true
        panel.begin { (result) in
            if result == .OK,
                let url = panel.urls.first,
                let image = NSImage(contentsOf: url) {
                completion(.success(image))
            } else {
                completion(.failure(
                    NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to get file location"])
                ))
            }
        }
    }
}

The SwiftUI Views


struct InputView: View {
    
    @Binding var image: NSImage?
    
    var body: some View {
        VStack(spacing: 16) {
            HStack {
                Text("Input Image (PNG,JPG,JPEG,HEIC)")
                Button(action: selectFile) {
                    Text("From Finder")
                }
            }
            InputImageView(image: self.$image)
        }
    }
    
    private func selectFile() {
        NSOpenPanel.openImage { (result) in
            if case let .success(image) = result {
                self.image = image
            }
        }
    }
}

struct InputImageView: View {
    
    @Binding var image: NSImage?
        
    var body: some View {
        ZStack {
            if self.image != nil {
                Image(nsImage: self.image!)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            } else {
                Text("Drag and drop image file")
                    .frame(width: 320)
            }
        }
        .frame(height: 320)
        .background(Color.black.opacity(0.5))
        .cornerRadius(8)
            
        .onDrop(of: ["public.url","public.file-url"], isTargeted: nil) { (items) -> Bool in
            if let item = items.first {
                if let identifier = item.registeredTypeIdentifiers.first {
                    print("onDrop with identifier = \(identifier)")
                    if identifier == "public.url" || identifier == "public.file-url" {
                        item.loadItem(forTypeIdentifier: identifier, options: nil) { (urlData, error) in
                            DispatchQueue.main.async {
                                if let urlData = urlData as? Data {
                                    let urll = NSURL(absoluteURLWithDataRepresentation: urlData, relativeTo: nil) as URL
                                    if let img = NSImage(contentsOf: urll) {
                                        self.image = img
                                        print("got it")
                                    }
                                }
                            }
                        }
                    }
                }
                return true
            } else { print("item not here") return false }
        }
    }    
}

Note: Have not needed to use the "public.image" identifier.

Optional Extensions if you need the result as PNG data (I did to upload to Firebase Storage):

extension NSBitmapImageRep {
    var png: Data? { representation(using: .png, properties: [.compressionFactor:0.05]) }
}

extension Data {
    var bitmap: NSBitmapImageRep? { NSBitmapImageRep(data: self) }
}

extension NSImage {
    var png: Data? { tiffRepresentation?.bitmap?.png }
}

// usage

let image = NSImage(...)
if let data = image.png {
     // do something further with the data
}
like image 25
Zach Grimaldi Avatar answered Nov 01 '22 13:11

Zach Grimaldi


Can’t you use onDrop(of:isTargeted:perform:)? You can pass your array of supported types in the of argument.

like image 36
Snowy_1803 Avatar answered Nov 01 '22 14:11

Snowy_1803