Implementing a Drag-And-Drop Zone in Swift

Implementing a drag-and-drop zone in Swift

Here's an example I'm using in an application.

  1. Add conformance to NSDraggingDestination to your subclass declaration if necessary (not needed for NSImageView because it already conforms to the protocol)
  2. Declare an array of accepted types (at least NSFilenamesPboardType)
  3. Register these types with registerForDraggedTypes
  4. Override draggingEntered, draggingUpdated and performDragOperation
  5. Return an NSDragOperation from these methods
  6. Get the file(s) path(s) from the draggingPasteboard array

In my example I've added a function to check if the file extension is amongst the ones we want.

Swift 2

class MyImageView: NSImageView {

override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
}

required init?(coder: NSCoder) {
super.init(coder: coder)
// Declare and register an array of accepted types
registerForDraggedTypes([NSFilenamesPboardType, NSURLPboardType, NSPasteboardTypeTIFF])
}

let fileTypes = ["jpg", "jpeg", "bmp", "png", "gif"]
var fileTypeIsOk = false
var droppedFilePath: String?

override func draggingEntered(sender: NSDraggingInfo) -> NSDragOperation {
if checkExtension(sender) {
fileTypeIsOk = true
return .Copy
} else {
fileTypeIsOk = false
return .None
}
}

override func draggingUpdated(sender: NSDraggingInfo) -> NSDragOperation {
if fileTypeIsOk {
return .Copy
} else {
return .None
}
}

override func performDragOperation(sender: NSDraggingInfo) -> Bool {
if let board = sender.draggingPasteboard().propertyListForType("NSFilenamesPboardType") as? NSArray,
imagePath = board[0] as? String {
// THIS IS WERE YOU GET THE PATH FOR THE DROPPED FILE
droppedFilePath = imagePath
return true
}
return false
}

func checkExtension(drag: NSDraggingInfo) -> Bool {
if let board = drag.draggingPasteboard().propertyListForType("NSFilenamesPboardType") as? NSArray,
path = board[0] as? String {
let url = NSURL(fileURLWithPath: path)
if let fileExtension = url.pathExtension?.lowercaseString {
return fileTypes.contains(fileExtension)
}
}
return false
}
}

Swift 3

class MyImageView: NSImageView {

override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}

required init?(coder: NSCoder) {
super.init(coder: coder)
// Declare and register an array of accepted types
register(forDraggedTypes: [NSFilenamesPboardType, NSURLPboardType, NSPasteboardTypeTIFF])
}

let fileTypes = ["jpg", "jpeg", "bmp", "png", "gif"]
var fileTypeIsOk = false
var droppedFilePath: String?

override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
if checkExtension(drag: sender) {
fileTypeIsOk = true
return .copy
} else {
fileTypeIsOk = false
return []
}
}

override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
if fileTypeIsOk {
return .copy
} else {
return []
}
}

override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
if let board = sender.draggingPasteboard().propertyList(forType: "NSFilenamesPboardType") as? NSArray,
imagePath = board[0] as? String {
// THIS IS WERE YOU GET THE PATH FOR THE DROPPED FILE
droppedFilePath = imagePath
return true
}
return false
}

func checkExtension(drag: NSDraggingInfo) -> Bool {
if let board = drag.draggingPasteboard().propertyList(forType: "NSFilenamesPboardType") as? NSArray,
path = board[0] as? String {
let url = NSURL(fileURLWithPath: path)
if let fileExtension = url.pathExtension?.lowercased() {
return fileTypes.contains(fileExtension)
}
}
return false
}
}

Get file path using drag and drop (Swift macOS)?

[Update to Swift 4.0 and Xcode 9]

Inspired by Implementing a drag-and-drop zone in Swift

Add a NSView to your main view and subclass.
This code works perfect in Swift 4.0 and macOS 10.13 High Sierra!

import Cocoa

class DropView: NSView {

var filePath: String?
let expectedExt = ["kext"] //file extensions allowed for Drag&Drop (example: "jpg","png","docx", etc..)

required init?(coder: NSCoder) {
super.init(coder: coder)

self.wantsLayer = true
self.layer?.backgroundColor = NSColor.gray.cgColor

registerForDraggedTypes([NSPasteboard.PasteboardType.URL, NSPasteboard.PasteboardType.fileURL])
}

override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
}

override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
if checkExtension(sender) == true {
self.layer?.backgroundColor = NSColor.blue.cgColor
return .copy
} else {
return NSDragOperation()
}
}

fileprivate func checkExtension(_ drag: NSDraggingInfo) -> Bool {
guard let board = drag.draggingPasteboard().propertyList(forType: NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")) as? NSArray,
let path = board[0] as? String
else { return false }

let suffix = URL(fileURLWithPath: path).pathExtension
for ext in self.expectedExt {
if ext.lowercased() == suffix {
return true
}
}
return false
}

override func draggingExited(_ sender: NSDraggingInfo?) {
self.layer?.backgroundColor = NSColor.gray.cgColor
}

override func draggingEnded(_ sender: NSDraggingInfo) {
self.layer?.backgroundColor = NSColor.gray.cgColor
}

override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
guard let pasteboard = sender.draggingPasteboard().propertyList(forType: NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")) as? NSArray,
let path = pasteboard[0] as? String
else { return false }

//GET YOUR FILE PATH !!!
self.filePath = path
Swift.print("FilePath: \(path)")

return true
}
}

To use this code you have to set "macOS Deployment Target" to 10.13

screenshot

How do you determine the drag and drop location in Swift 5?

class ViewController: UIViewController {

let bounds = UIScreen.main.bounds
let imageViewWidth: CGFloat = 100
let imageViewHeight: CGFloat = 200
let inset: CGFloat = 40

var arrayDropZones = [DropZoneCard]()

var initialFrame: CGRect {
get {
return CGRect(x: bounds.width - imageViewWidth,
y: bounds.height - imageViewHeight,
width: imageViewWidth,
height: imageViewHeight
)
}

}

override func viewDidLoad() {
super.viewDidLoad()

addDropZones()
addNewCard()
}
}

extension ViewController {
func addDropZones() {
let dropZone1 = getDropZoneCard()
dropZone1.frame = CGRect(x: inset, y: inset, width: imageViewWidth, height: imageViewHeight)

let dropZone2 = getDropZoneCard()
let x = bounds.width - imageViewWidth - inset
dropZone2.frame = CGRect(x: x, y: inset, width: imageViewWidth, height: imageViewHeight)

let dropZone3 = getDropZoneCard()
let y = inset + imageViewHeight + inset
dropZone3.frame = CGRect(x: inset, y: y, width: imageViewWidth, height: imageViewHeight)

let dropZone4 = getDropZoneCard()
dropZone4.frame = CGRect(x: x, y: y, width: imageViewWidth, height: imageViewHeight)


[dropZone1, dropZone2, dropZone3, dropZone4].forEach {
view.addSubview($0)
self.arrayDropZones.append($0)
}
}

func getNewCard() -> UIImageView {
let imageView = UIImageView()
imageView.isUserInteractionEnabled = true
imageView.backgroundColor = .green

imageView.frame = initialFrame

let panGesture = UIPanGestureRecognizer(target: self, action:(#selector(handleGesture(_:))))
imageView.addGestureRecognizer(panGesture)

return imageView
}

func getDropZoneCard() -> DropZoneCard {
let dropZone = DropZoneCard()
dropZone.isUserInteractionEnabled = true
dropZone.backgroundColor = .yellow
return dropZone
}

func addNewCard() {
let imageView = getNewCard()
view.addSubview(imageView)
}

@objc func handleGesture(_ recognizer: UIPanGestureRecognizer) {

let translation = recognizer.translation(in: self.view)
if let view = recognizer.view {

view.center = CGPoint(x:view.center.x + translation.x,
y:view.center.y + translation.y)

if recognizer.state == .ended {
let point = view.center
for dropZone in arrayDropZones {
if dropZone.frame.contains(point) {
dropZone.append(card: view)
addNewCard()
return
}
}

view.frame = initialFrame
}
}

recognizer.setTranslation(.zero, in: view)
}
}

class DropZoneCard: UIImageView {
private(set) var arrayCards = [UIView]()

func append(card: UIView) {
arrayCards.append(card)
card.isUserInteractionEnabled = false
card.frame = frame
}
}

SwiftUI Drag and Drop files

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)
}
}

Xcode Swift MacOS App, drag and drop file into NSTextField

First of all, you need to read this guide.

Second, I post here some code that I use to do something similar to what you are asking.

However, my strategy is not to subclass NSTextField but rather place this field inside an NSBox, which I subclass. This has the advantage of providing to the user some visual feedback using a focus ring.

Pay attention to performDragOperation where the string value is set via the window's controller, which then forwards it to the text field to set its string value to the path to the dropped file.

You can filter what you can accept by prepareForDragOperation. Check that too.

class DropBox: NSBox
{
let dragType = NSPasteboard.PasteboardType(kUTTypeFileURL as String)
var doHighlight = false

// ---------------------------------------------------------------------------------
// awakeFromNib
// ---------------------------------------------------------------------------------
override func awakeFromNib()
{
registerForDraggedTypes([dragType])
}

// ---------------------------------------------------------------------------------
// acceptsFirstMouse
// ---------------------------------------------------------------------------------
// Accept activation click as click in window, so source doesn't have to be the
// active window
override func acceptsFirstMouse(for event: NSEvent?) -> Bool
{
return true
}

// ---------------------------------------------------------------------------------
// draggingEntered
// ---------------------------------------------------------------------------------
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation
{
let pasteboard = sender.draggingPasteboard
let mask = sender.draggingSourceOperationMask

if let types = pasteboard.types, types.contains(dragType)
{
if mask.contains(.link)
{
doHighlight = true
needsDisplay = true
return .link
}
}

return []
}

// ---------------------------------------------------------------------------------
// draggingExited
// ---------------------------------------------------------------------------------
override func draggingExited(_ sender: NSDraggingInfo?)
{
doHighlight = false
needsDisplay = true
}

// ---------------------------------------------------------------------------------
// drawRect
// ---------------------------------------------------------------------------------
override func draw(_ dirtyRect: NSRect)
{
super.draw(dirtyRect)

if doHighlight {
let rect = NSRect(x: dirtyRect.origin.x,
y: dirtyRect.origin.y,
width: NSWidth(dirtyRect),
height: NSHeight(dirtyRect) - NSHeight(titleRect) + 1.0)

NSFocusRingPlacement.only.set()
let contentRect = rect.insetBy(dx: 4, dy: 4)
NSBezierPath(rect: contentRect).fill()
}
}

// ---------------------------------------------------------------------------------
// performDragOperation
// ---------------------------------------------------------------------------------
// Method to handle drop data
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool
{
if let source = sender.draggingSource as? NSBox {
if source === self {
return false
}
}

let pasteboard = sender.draggingPasteboard
let options = [NSPasteboard.ReadingOptionKey.urlReadingFileURLsOnly:true]
if let urls = pasteboard.readObjects(forClasses: [NSURL.self], options: options) as? [URL],
let controller = self.window?.delegate as? WindowController
{
for url in urls {
if SchISCoreFileUtilities.isValid(url.path) {
controller.setApplicationPath(url.path)
return true
}
}
}

return false
}

// ---------------------------------------------------------------------------------
// prepareForDragOperation
// ---------------------------------------------------------------------------------
// Method to determine if we can accept the drop (filter for urls to apps)
override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool
{
doHighlight = false
needsDisplay = true
let pasteboard = sender.draggingPasteboard

if let types = pasteboard.types, types.contains(dragType)
{
let options = [NSPasteboard.ReadingOptionKey.urlReadingFileURLsOnly:true]
if let urls = pasteboard.readObjects(forClasses: [NSURL.self], options: options) as? [URL]
{
for url in urls {
if url.pathExtension == "app" {
return true
}
}
}
}

return false
}

}

Basic Drag and Drop in iOS

Assume you have a UIView scene with a background image and many vehicles, you may define each new vehicle as a UIButton (UIImageView will probably work too):

UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button addTarget:self action:@selector(imageTouch:withEvent:) forControlEvents:UIControlEventTouchDown];
[button addTarget:self action:@selector(imageMoved:withEvent:) forControlEvents:UIControlEventTouchDragInside];
[button setImage:[UIImage imageNamed:@"vehicle.png"] forState:UIControlStateNormal];
[self.view addSubview:button];

Then you may move the vehicle wherever you want, by responding to the UIControlEventTouchDragInside event, e.g.:

- (IBAction) imageMoved:(id) sender withEvent:(UIEvent *) event
{
CGPoint point = [[[event allTouches] anyObject] locationInView:self.view];
UIControl *control = sender;
control.center = point;
}

It's a lot easier for individual vehicle to handle its own drags, comparing to manage the scene as a whole.



Related Topics



Leave a reply



Submit