Kodeco Forums

Drag and Drop Tutorial for macOS

The drag-and-drop mechanism has always been an integral part of Macs. Learn how to adopt it in your apps with this drag and drop tutorial for macOS.


This is a companion discussion topic for the original entry at https://www.raywenderlich.com/1016-drag-and-drop-tutorial-for-macos
1 Like

First of all a good and comprehensive tutorial (as usual) which gives a good head start for a future project.

One thing that hampered me during the tutorial is the function shaphot() instead of snapshot() (used in the mouseDown functions)

I like to type with the tutorials instead of using copy/paste. Therefore i had to reread the sentence multiple times before i discovered why i got an error. I assume it is a typo, if this is not the case a explanation or comment would be nice

Hi - that is a typo in the function name in the project. The copy/paste code is correct. Ill get that fixed soon :slight_smile:

Why are two different kinds needed to check whether the data can be dragged and dropped? We already define the types we accept in
register(forDraggedTypes: Array(acceptableTypes))
why do we actually have to check the pasteboard a 2nd time in shouldAllowDrag(_:)

Hi Christopher

Its a matter of refining what you want from the dragging session. So you define one of the acceptable types as URL so any URL but then in shouldAllowDrag(_:) you filter out to only URLs that are images using the criteria NSImage.imageTypes() . Think of it as a bag of M&M’s and you only want the yellow ones :]

Great tutorial, as always!

Do you have any pointers on displaying a modal window in the performDragOperation() method? What I found is that I cannot enter text in an NSTextField of a modal window because the drag operation is not complete. After some time, it seems to time out, at which point I can enter text in the modal window. Clicking works, as things like NSAlert() run modally allow clicking the buttons.

I’ve tried a variety of dispatch_*() methods, none of which helped.

I see what you mean. What I had to do to get this working was to do async dispatch into the main queue.

So as a quick hack i added a new Window into the storyboard and this to the main body of StickerBoardViewController

var secondary:NSWindowController!
  func showAModalWindow() {
    if let mwindow = self.storyboard?.instantiateController(withIdentifier: "Secondary") as? NSWindowController {
      secondary = mwindow
      NSApp.runModal(for: secondary.window!)
    }
  }

and then this to the start of processAction(_:center:)

 DispatchQueue.main.async(execute:{
      self.showAModalWindow()
    })

The drag operation completes and the Modal window presents. A textfield in that window was fully interactive.

Warren, I really appreciate you taking the time to answer! I was surprised to hear this worked for you as the first thing I tried was dispatch_async to the main thread. I added this new code to the completed project and found two things:

  1. With what you implemented, you actually don’t even need to async dispatch to main
  2. The big difference between what you implemented and the problem I’m having is that performAction() is only triggered from a drag source inside the application.

My test case is dragging a file URL from Finder (or elsewhere) into my app. Fortunately, your tutorial has the perfect place to evaluate this case. I moved the DispatchQueue.main.async snippet into processImageURLs(), which handles JPEGs dropped from Finder, and was able to confirm that dispatch async no longer solves the problem…

It actually worked once out of several runs. I believe using dispatch_async is creating a race condition. If the drag happens to finish first, the text fields are responsive. If the drag does not finish before the new window is made key then text fields cannot respond.

Im getting a similar result as you when i attempt to pop up the modal window at the end of processImageURLs . The window appears and can receive events but the Finder spins-locks and eventually the drag times out and the drag images slide back.

Its a very naive-implementation in the tutorial which loads the images from URL in the main thread (you can spin lock the app by dumping a large number of images in at once) so im wondering if you load the images in a background thread then do a final render in the main thread it might sort it out.

Nice bug to figure out :smile:

Yes - If you throw the image generation out into the background it works itself out. Passes the test cases of the modal receiving events and the Finder not spin locking. I threw a dozen large images at it and it seems to work.

func processImageURLs(_ urls: [URL], center: NSPoint) {
    
    DispatchQueue.global(qos: .background).async {
      
      var images:[NSImage] = []
      
      for url in urls {
        
        //1.
        if let image = NSImage(contentsOf:url) {
          images.append(image)
        }
      }
      
      DispatchQueue.main.async {
        for (index,image) in images.enumerated() {
          var newCenter = center
          //2.
          if index > 0 {
            newCenter = center.addRandomNoise(Appearance.randomNoise)
          }
          //3.
          self.processImage(image, center:newCenter)
        }
        self.showAModalWindow()
      }
      
    }
  }

I’m wondering why you added an additional View as the drag target? Especially as it was over the top of StickerBoardView.

Could the StickerBoardViewController not detect and handle the drag events itself?

Was it to show that you can drag and drop to a separate view to the one you want to actually effect or is there another reason?

Good question. A couple of reasons.

As you mention I wanted to demonstrate a delegation/decoupling pattern by having a utility view that just handled the dragging while handing off its relevant events to another object.

The other reason was that I was initially going to do some painting (drawing of the sticker outline) in that view which went over the top of everything else but the tutorial got too long and I had to cut the feature.

Also you are still going to need to make a custom NSView to handle the NSDraggingDestination events. You dont register view controllers for dragging events. Only NSView and NSWindow can do that.

Thank you so much ! This realy helped alot

Can you release an Objective-C-Version of the Tutorial?
Swift is too ugly for me.

Hey Warren!

To read the URLs of dragging files, without dragging them into the view controller, should be possible. But therefore the DestinationView should not be declared as a NSView-class. Is that right?
Which class I have to use, to read the dragged file URL in the same time, my left mouse clicks on the file, which is located outside my app?

Thx

Hi Tom - I do like Objective-C too. I spend my working day in a large mixed codebase of OC and Swift and sometimes Swift just gets a bit unreadable especially when a developer gets a bit too clever.

The Razeware site is exclusively Swift for Apple development so you are unlikely to see a OC version of this tutorial. Try rewriting the code in the tutorial as objective-c. Its not much different for a simple API based project like this and its good practise for working with legacy code.

No, it shouldn’t be possible. Dragging is a secure user initiated action.

If the destination view wasn’t a NSView it couldn’t be part of the view hierarchy.

As for knowing what file has received a right click in Finder. Why do you think Finder would supply this information to the OS. That wouldn’t be very secure.

So, that sounds difficult.

Can you explain me, how finder handles the drag and drop cooperative with OSX, respectively a good Swift 3 manual for “Outside my app drag-and-drop-listener”.
Like your tutorial, I found many excellent explained tutorials for drag and drop. Also I found a Objective-C tutorial for that from apple, wich I was not able to translate to Swift 3, what I tried.

By the way, thanks for your very quick answer, so far and I’m extremely curiousing to your next answer! :wink:

Finder is just an application like any other with respect to Drag and Drop. It registers itself with macOS for certain types and vends certain types when you drag from it just like you did in the tutorial.

If you have a look at https://www.raywenderlich.com/165853/menus-popovers-menu-bar-apps-macos it describes how to set up a global event monitor on NSEvent in the EventMonitor class. You might be able to set up a listener to dragging events and from there check the Dragging Pasteboard (NSDragPboard), which is one of the global named pasteboards for content. This might work , or not. I dont know. Its what i’d explore if I was trying to do this.