Implementing a drag-and-drop zone in Swift
Here's an example I'm using in an application.
- Add conformance to
NSDraggingDestination
to your subclass declaration if necessary (not needed forNSImageView
because it already conforms to the protocol) - Declare an array of accepted types (at least
NSFilenamesPboardType
) - Register these types with
registerForDraggedTypes
- Override
draggingEntered
,draggingUpdated
andperformDragOperation
- Return an
NSDragOperation
from these methods - 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
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
How to Split an Int to Its Individual Digits
Swiftui: How to Present View When Clicking on a Button
How to Assign an Optional Binding Parameter in Swiftui
How to Use Alamofire with Custom Headers for Post Request
How to Improve Camera Quality in Arkit
Obj-C Cocoapods + Swift Framework
Exponentiation Operator in Swift
Swift's JSONdecoder with Multiple Date Formats in a JSON String
Prefer Large Titles and Refreshcontrol Not Working Well
Get the Last Character of a String Without Using Array
Swiftui Hide Tabbar in Subview
Get the Size (In Bytes) of an Object on the Heap
Writing Data to an Nsoutputstream in Swift 3
How to Change Font Size and Font Name of Uisegmentedcontrol Programmatically on Swift