Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

Drag and Drop Tutorial for macOS

$
0
0
Drag_and_Drop_Tutorial_for_macOS

Learn all about drag and drop for macOS!

Ever since the invention of the Mac, drag and drop has been a part of the user interface. The quintessential example is in Finder, where you can drag files around to organize things or drop them in the trash.

The fun doesn’t stop there.

You can drag your latest sunset panorama from Photos and drop it in Messages, or pull a file from Downloads on the dock and drop it right in an email. You get the point, right? It’s cool and an integral part of the macOS experience.

Drag and drop has come a long way from its beginnings and now you can drag almost anything anywhere. Try it and you might be pleasantly surprised by the actions and types supported by your favorite apps.

In this drag and drop tutorial for macOS, you’ll discover how to add support to your own apps, so users can get the full Mac experience with your app.

Along the way, you’ll learn how to:

  • Implement core drag and drop actions on NSView subclasses
  • Accept data dropped from other applications
  • Provide data to be dragged into other views of your app
  • Create custom dragged types

Getting Started

This project uses Swift 3 and requires, at a minimum, Xcode 8 beta 4. Download the starter project, open it in Xcode and build and run it.

window-starting

Meet the Project App

Many kids enjoy playing with stickers and making cool collages with them, so you’re going to build an app that features this experience. You can drag images onto a surface and then you can kick things up a few notches by adding sparkles and unicorns to the view.

After all, who doesn’t like unicorns and sparkles? :]

To keep you focused on the objective — building out support for dragging and dropping — the starter project comes complete with the views you’ll require. All you need to do is learn about the mechanics of drag and drop.

There are three parts to the project window:

  • The sticker view where you’ll drag images and other things
  • Two smaller views that you’ll turn into different dragging sources

Take a look at the project.

project-display
You’ll edit four specific files as you work through this tutorial, and they’re in two places: Dragging Destinations and Dragging Sources:

In Dragging Destinations:

  • StickerBoardViewController.swift: the main view controller
  • DestinationView.swift: view that sits on top of the upper section of the window — it will be the recipient of your drag actions

view-hierachy

In Dragging Sources:

  • ImageSourceView.swift: bottom view with the unicorn image that you’ll turn into a dragging source
  • AppActionSourceView.swift: view that has the label Sparkles — you’ll turn this into another type of dragging source

There are some other files in the Drawing and Other Stuff groups that provide helper methods and are essential to the project app, but you won’t need to give them any of your time. Go ahead and explore if you’d like to see how this thing is built!

Pasteboards and Dragging Sessions

Drag and drop involves a source and a destination.

You drag an item from a source, which needs to implement the NSDraggingSource protocol. Then you drop it into a destination, which must implement the NSDraggingDestination protocol in order to accept or reject the items received. NSPasteboard is the class that facilitates the exchange of data.

The whole process is known as a dragging session:

dragging session macroscopic view

When you drag something with your mouse, e.g., a file, the following happens:

  1. A dragging session kicks off when you initiate a drag.
  2. Some data bits — often an image and URL — are chosen to represent the information placed on the dragging pasteboard.
  3. You drop that image on a destination, which chooses to reject or accept it and take some action — for instance, move the file to a new folder.
  4. The dragging session concludes.

That’s pretty much the gist of it. It’s a pretty simple concept!

First up is creating a dragging destination for receiving images from Finder or any other app.

Creating a Dragging Destination

A dragging destination is a view or window that accepts types of data from the dragging pasteboard. You create a dragging destination by adopting NSDraggingDestination.

This diagram shows the anatomy of a dragging session from the point of view of a dragging destination.
dragging session

There are a few steps involved in creating the destination:

  1. When building the view, you have to declare the types that it should receive from any dragging session.
  2. When a dragging image enters the view, you need to implement logic to allow the view to decide whether to use the data as well as let the dragging session know its decision.
  3. When the dragging image lands on the view, you use data from the dragging pasteboard to show it on your view.

Time to get down with some codeness!

Select DestinationView.swift in the project navigator and locate the following method:

func setup() {
}

Replace it with this:

var acceptableTypes: Set<String> { return [NSURLPboardType] }
 
func setup() {
  register(forDraggedTypes: Array(acceptableTypes))
}

This code defines a set with the supported types. In this case, URLs. Then, it calls register(forDraggedTypes:) to accept drags that contain those types.

Add the following code into DestinationView to analyze the dragging session data:

//1.
let filteringOptions = [NSPasteboardURLReadingContentsConformToTypesKey:NSImage.imageTypes()]
 
func shouldAllowDrag(_ draggingInfo: NSDraggingInfo) -> Bool {
 
  var canAccept = false
 
  //2.
  let pasteBoard = draggingInfo.draggingPasteboard()
 
  //3.
  if pasteBoard.canReadObject(forClasses: [NSURL.self], options: filteringOptions) {
    canAccept = true
  }
  return canAccept
 
}

You’ve done a few things in here:

  1. Created a dictionary to define the desired URL types (images).
  2. Got a reference to the dragging pasteboard from the dragging session info.
  3. Asked pasteboard if it has any URLs and whether those URLs are references to images. If it has images, it accepts the drag. Otherwise, it rejects it.

NSDraggingInfo is a protocol that declares methods to supply information about the dragging session. You don’t create them, nor do you store them between events. The system creates the necessary objects during the dragging session.

You can use this information to provide feedback to the dragging session when the app receives the image.

NSView conforms to NSDraggingDestination, so you need to override the draggingEntered(_:) method by adding this code inside the DestinationView class implementation:

//1.
var isReceivingDrag = false {
  didSet {
    needsDisplay = true
  }
}
 
//2.
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
  let allow = shouldAllowDrag(sender)
  isReceivingDrag = allow
  return allow ? .copy : NSDragOperation()
}

This is what the code above does:

  1. Creates a property named isReceivingDrag to track when the dragging session is inside the view and has data that you want. It triggers a redraw on the view each time it is set.
  2. Overrides the draggingEntered(_:) , and decides if it accepts the drag operation.

In section two, the method needs to return an NSDragOperation. You have probably noticed how the mouse pointer changes depending on the keys you hold down or the destination of a drag.

For example, if you hold down Option during a Finder drag, the pointer gains a green + symbol to show you a file copy is about to happen. This value is how you control those pointer changes.

In this config, if the dragging pasteboard has an image then it returns .copy to show the user that you’re about to copy the image. Otherwise it returns NSDragOperation() if it doesn’t accept the dragged items.

Handling an Exit

What enters the view may also exit, so the app needs to react when a dragging session has exited your view without a drop. Add the following code:

override func draggingExited(_ sender: NSDraggingInfo?) {
  isReceivingDrag = false
}

You’ve overridden draggingExited(_:) and set the isReceivingDrag variable to false.

Tell the User What’s Happening

You’re almost done with the first stretch of coding! Users love to see a visual cue when something is happening in the background, so the next thing you’ll add is a little drawing code to keep your user in the loop.

Still in DestinationView.swift, find draw(:_) and replace it with this.

override func draw(_ dirtyRect: NSRect) {
 
  if isReceivingDrag {
    NSColor.selectedControlColor.set()
 
    let path = NSBezierPath(rect:bounds)
    path.lineWidth = Appearance.lineWidth
    path.stroke()
  }
}

This code draws a system-colored border when a valid drag enters the view. Aside from looking sharp, it makes your app consistent with the rest of the system by providing a visual when it accepts a dragged item.

Note: Want to know more about custom drawing? Check out our Core Graphics on macOS Tutorial.

Build and run then try dragging an image file from Finder to StickerDrag. If you don’t have an image handy, use sample.jpg inside the project folder.

buildrun-add-plus

You can see that the cursor picks up a + symbol when inside the view and that the view draws a border around it.

When you exit the view, the border and + disappears; absolutely nothing happens when you drag anything but an image file.

Wrap up the Drag

Now, on to the final step for this section: You have to accept the drag, process the data and let the dragging session know that this has occurred.

Append the DestinationView class implementation with the following:

override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
  let allow = shouldAllowDrag(sender)
  return allow
}

The system calls the above method when you release the mouse inside the view; it’s the last chance to reject or accept the drag. Returning false will reject it, causing the drag image to slide back to its origination. Returning true means the view accepts the image. When accepted, the system removes the drag image and invokes the next method in the protocol sequence: performDragOperation(_:).

Add this method to DestinationView:

override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool {
 
  //1.
  isReceivingDrag = false
  let pasteBoard = draggingInfo.draggingPasteboard()
 
  //2.
  let point = convert(draggingInfo.draggingLocation(), from: nil)
  //3.
  if let urls = pasteBoard.readObjects(forClasses: [NSURL.self], options:filteringOptions) as? [URL], urls.count > 0 {
    delegate?.processImageURLs(urls, center: point)
    return true
  }
  return false
 
}

Here’s what you’re doing in there:

  1. Reset isReceivingDrag flag to false.
  2. Convert the window-based coordinate to a view-relative coordinate.
  3. Hand off any image URLs to the delegate for processing, and return true — else you reject the drag operation returning false.

Note: Feeling extra heroic? If you were to make an animated drop sequence, performDragOperation(:_) would be the best place to start the animation.

Congratulations! You’ve just finished the first section and have done all the work DestinationView needs to receive a drag.

Use DestinationView’s Data

Next up you’ll use the data that DestinationView provides in its delegate.

Open StickerBoardViewController.swift and introduce yourself to the class that is the delegate of DestinationView.

To use it properly, you need to implement the DestinationViewDelegate method that places the images on the target layer. Find processImage(_:center:) and replace it with this.

func processImage(_ image: NSImage, center: NSPoint) {
 
  //1.
  invitationLabel.isHidden = true
 
  //2.
  let constrainedSize = image.aspectFitSizeForMaxDimension(Appearance.maxStickerDimension)
 
  //3.
  let subview = NSImageView(frame:NSRect(x: center.x - constrainedSize.width/2, y: center.y - constrainedSize.height/2, width: constrainedSize.width, height: constrainedSize.height))
  subview.image = image
  targetLayer.addSubview(subview)
 
  //4.
  let maxrotation = CGFloat(arc4random_uniform(Appearance.maxRotation)) - Appearance.rotationOffset
  subview.frameCenterRotation = maxrotation
 
}

This code does the following tricks:

  1. It hides the Drag Images Here label.
  2. It figures out the maximum size for the dropped image while holding the aspect ratio constant.
  3. It constructs a subview with that size, centers it on the drop point and adds it to the view hierarchy.
  4. It randomly rotates the view a little bit for a bit of funkiness.

With all that in place, you’re ready to implement the method so it deals with the image URLs that get dragged into the view.
Replace processImageURLs(_:center:) method with this:

func processImageURLs(_ urls: [URL], center: NSPoint) {
  for (index,url) in urls.enumerated() {
 
    //1.
    if let image = NSImage(contentsOf:url) {
 
      var newCenter = center
      //2.
      if index > 0 {
        newCenter = center.addRandomNoise(Appearance.randomNoise)
      }
 
      //3.
      processImage(image, center:newCenter)
    }
  }
}

What you’re doing here is:

  1. Creating an image with the contents from the URLs.
  2. If there is more than one image, this offsets the images’ centers a bit to create a layered, randomized effect.
  3. Pass the image and center point to the previous method so it can add the image to the view.

Now build and run then drag an image file (or several) to the app window. Drop it!

window-demo-1

Look at that board of images just waiting to be made fearlessly fanciful.

You’re at about the halfway point and have already explored how to make any view a dragging destination and how to compel it to accept a standard dragging type — in this case, an image URL.

Intermission: let's all go to the lobby and get ourselves some drinks. And snacks. And new iMacs

Creating a Dragging Source

You’ve played around with the receiving end, but how about the giving end?

In this section, you’ll learn how to supercharge your app with the ability to be the source by letting those unicorns and sparkles break free and bring glee to the users’ images in the right circumstances.

All dragging sources must conform to the NSDraggingSource protocol. This MVP (most valuable player) takes the task of placing data (or a promise for that data) for one or more types on the dragging pasteboard. It also supplies a dragging image to represent the data.

When the image finally lands on its target, the destination unarchives the data from the pasteboard. Alternatively, the dragging source can fulfil the promise of providing the data.

You’ll need to supply the data of two different types: a standard Cocoa type (an image) and custom type that you create.

Supplying a Standard Dragging Type

The dragging source will be ImageSourceView — the class of the view that has the unicorn. Your objective is simple: get that unicorn onto your collage.

The class needs to adopt the necessary protocols NSDraggingSource and NSPasteboardItemDataProvider, so open ImageSourceView.swift and add the following extensions:

// MARK: - NSDraggingSource
extension ImageSourceView: NSDraggingSource {
  //1.
  func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
    return .generic
  }
}
 
// MARK: - NSDraggingSource
extension ImageSourceView: NSPasteboardItemDataProvider {
  //2.
  func pasteboard(_ pasteboard: NSPasteboard?, item: NSPasteboardItem, provideDataForType type: String) {
    //TODO: Return image data
  }
}
  1. This method is required by NSDraggingSource. It tells the dragging session what sort of operation you’re attempting when the user drags from the view. In this case it’s a generic operation.
  2. This implements the mandatory NSPasteboardItemDataProvider method. More on this soon — for now it’s just a stub.

Start a Dragging Session

In a real world project, the best moment to initiate a dragging session depends on your UI.

With the project app, this particular view you’re working in exists for the sole purpose of dragging, so you’ll start the drag on mouseDown(with:).

In other cases, it may be appropriate to start in the mouseDragged(with:) event.

Add this method inside the ImageSourceView class implementation:

override func mouseDown(with theEvent: NSEvent) {
  //1.
  let pasteboardItem = NSPasteboardItem()
  pasteboardItem.setDataProvider(self, forTypes: [kUTTypeTIFF])
 
  //2.
  let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem)
  draggingItem.setDraggingFrame(self.bounds, contents:snaphot())
 
  //3.
  beginDraggingSession(with: [draggingItem], event: theEvent, source: self)
}

Things get rolling when the system calls mouseDown(with:) when you click on a view. The base implementation does nothing, eliminating the need to call super. The code in the implementation does all of this:

  1. Creates an NSPasteboardItem and sets this class as its data provider. A NSPasteboardItem is the box that carries the info about the item being dragged. The NSPasteboardItemDataProvider provides data upon request. In this case you’ll supply TIFF data, which is the standard way to carry images around in Cocoa.
  2. Creates a NSDraggingItem and assigns the pasteboard item to it. A dragging item exists to provide the drag image and carry one pasteboard item, but you don’t keep a reference to the item because of its limited lifespan. If needed, the dragging session will recreate this object. snapshot() is one of the helper methods mentioned earlier. It creates an NSImage of an NSView.
  3. Starts the dragging session. Here you trigger the dragging image to start following your mouse until you drop it.

Build and run. Try to drag the unicorn onto the top view.

buildrun-drag-unicorn

An image of the view follows your mouse, but it slides back on mouse up because DestinationView doesn’t accept TIFF data.

Take the TIFF

In order to accept this data, you need to:

  1. Update the registered types in setup() to accept TIFF data
  2. Update shouldAllowDrag() to accept the TIFF type
  3. Update performDragOperation(_:) to take the image data from the pasteboard

Open DestinationView.swift.

Replace the following line:

var acceptableTypes: Set<String> { return [NSURLPboardType] }

With this:

var nonURLTypes: Set<String>  { return [String(kUTTypeTIFF)] }
var acceptableTypes: Set<String> { return nonURLTypes.union([NSURLPboardType]) }

You’ve just registered the TIFF type like you did for URLs and created a subset to use next.

Next, go to shouldAllowDrag(:_), and add find the return canAccept method. Enter the following just above the return statement:

else if let types = pasteBoard.types, nonURLTypes.intersection(types).count > 0 {
  canAccept = true
}

Here you’re checking if the nonURLTypes set contains any of the types received from the pasteboard, and if that’s the case, accepts the drag operation. Since you added a TIFF type to that set, the view accepts TIFF data from the pasteboard.

Unarchive the Image Data

Lastly, update performDragOperation(_:) to unarchive the image data from the pasteboard. This bit is really easy.

Cocoa wants you to use pasteboards and provides an NSImage initializer that takes NSPasteboard as a parameter. You’ll find more of these convenience methods in Cocoa when you start exploring drag and drop more.

Locate performDragOperation(_:), and add the following code at the end, just above the return sentence return false:

else if let image = NSImage(pasteboard: pasteBoard) {
  delegate?.processImage(image, center: point)
  return true
}

This extracts an image from the pasteboard and passes it to the delegate for processing.

Build and run, and then drag that unicorn onto the sticker view.

buildrun-drag-unicorn-plus

You’ll notice that now you get a green + on your cursor.

The destination view accepts the image data, but the image still slides back when you drop. Hmmm. What’s missing here?

Show me the Image Data!

You need to get the dragging source to supply the image data — in other words: fulfil its promise.

Open ImageSourceView.swift and replace the contents of pasteboard(_:item:provideDataForType:) with this:

//1.
if let pasteboard = pasteboard, type == String(kUTTypeTIFF), let image = NSImage(named:"unicorn") {
  //2.
  let finalImage = image.tintedImageWithColor(NSColor.randomColor())
  //3.
  let tiffdata = finalImage.tiffRepresentation
  pasteboard.setData(tiffdata, forType:type)
}

In this method, the following things are happening:

  1. If the desired data type is kUTTypeTIFF, you load an image named unicorn.
  2. Use one of the supplied helpers to tint the image with a random color. After all, colorful unicorns are more festive than a smattering of all-black unicorns. :]
  3. Transform the image into TIFF data and place it on the pasteboard.

Build and run, and drag the unicorn onto the sticker view. It’ll drop and place a colored unicorn on the view. Great!

buildrun-add-unicorns

So.many.unicorns!

Dragging Custom Types

Unicorns are pretty fabulous, but what good are they without magical sparkles? Strangely, there’s no standard Cocoa data type for sparkles. I bet you know what comes next. :]

sparkle

Note: In the last section you supplied a standard data type. You can explore the types for standard data in the API reference.

In this section you’ll invent your own data type.

These are the tasks on your to-do list:

  1. Create a new dragging source with your custom type.
  2. Update the dragging destination to recognize that type.
  3. Update the view controller to react to that type.

Create the Dragging Source

Open AppActionSourceView.swift. It’s mostly empty except for this important definition:

enum SparkleDrag {
  static let type = "com.razeware.StickerDrag.AppAction"
  static let action = "make sparkles"
}

This defines your custom dragging type and action identifier.

Dragging source types must be Uniform Type Identifiers. These are reverse-coded name paths that describe a data type.

For example, if you print out the value of kUTTypeTIFF you’ll see that it is the string public.tiff.

To avoid a collision with an existing type, you can define the identifier like this: bundle identifier + AppAction. It is an arbitrary value, but you keep it under the private namespace of the application to minimize the risk of using an existing name.

If you attempt to construct a NSPasteboardItem with a type that isn’t UTI, the operation will fail.

Now you need to make AppActionSourceView adopt NSDraggingSource. Open AppActionSourceView.swift and add the following extension:

// MARK: - NSDraggingSource
extension AppActionSourceView: NSDraggingSource {
 
  func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor
    context: NSDraggingContext) -> NSDragOperation {
 
    switch(context) {
    case .outsideApplication:
      return NSDragOperation()
    case .withinApplication:
      return .generic
    }
  }
}

This code block differs from ImageSourceView because you’ll place private data on the pasteboard that has no meaning outside the app. That’s why you’re using the context parameter to return a NSDragOperation() when the mouse is dragged outside your application.

You’re already familiar with the next step. You need to override the mouseDown(with:) event to start a dragging session with a pasteboard item.

Add the following code into the AppActionSourceView class implementation:

override func mouseDown(with theEvent: NSEvent) {
 
  let pasteboardItem = NSPasteboardItem()
  pasteboardItem.setString(SparkleDrag.action, forType: SparkleDrag.type)
  let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem)
  draggingItem.setDraggingFrame(self.bounds, contents:snaphot())
 
  beginDraggingSession(with: [draggingItem], event: theEvent, source: self)
 
}

What did you do in there?

You constructed a pasteboard item and placed the data directly inside it for your custom type. In this case, the data is a custom action identifier that the receiving view may use to make a decision.

You can see how this differs from ImageSourceView in one way. Instead of deferring data generation to the point when the view accepts the drop with the NSPasteboardItemDataProvider protocol, the dragged data goes directly to the pasteboard.

Why would you use the NSPasteboardItemDataProvider protocol? Because you want things to move as fast as possible when you start the drag session in mouseDown(with:).

If the data you’re moving takes too long to construct on the pasteboard, it’ll jam up the main thread and frustrate users with a perceptible delay when they start dragging.

In this case, you place a small string on the pasteboard so that it can do it right away.

Accept the New Type

Next, you have to let the destination view accept this new type. By now, you already know how to do it.

Open DestinationView.swift and add SparkleDrag.type to the registered types. Replace the following line:

var nonURLTypes: Set<String>  { return [String(kUTTypeTIFF)] }

With this:

var nonURLTypes: Set<String>  { return [String(kUTTypeTIFF),SparkleDrag.type] }

Now SparkleDrags are acceptable!

performDragOperation(:_) needs a new else-if clause, so add this code at the end of the method just before return false:

else if let types = pasteBoard.types, types.contains(SparkleDrag.type),
  let action = pasteBoard.string(forType: SparkleDrag.type) {
  delegate?.processAction(action, center:point)
  return true
}

This addition extracts the string from the pasteboard. If it corresponds to your custom type, you pass the action back to the delegate.

You’re almost done, you just need to update StickerBoardViewController to deal with the action instruction.

Handle the Action Instruction

Open StickerBoardViewController.swift and replace processAction(_:center:) with this:

func processAction(_ action: String, center: NSPoint) {
  //1.
  if action == SparkleDrag.action  {
    invitationLabel.isHidden = true
 
    //2.
    if let image = NSImage(named:"star") {
 
      //3.
      for _ in 1..<Appearance.numStars {
 
        //A.
        let maxSize:CGFloat = Appearance.maxStarSize
        let sizeChange = CGFloat(arc4random_uniform(Appearance.randonStarSizeChange))
        let finalSize = maxSize - sizeChange
        let newCenter = center.addRandomNoise(Appearance.randomNoiseStar)
 
        //B.
        let imageFrame = NSRect(x: newCenter.x, y: newCenter.y, width: finalSize , height: finalSize)
        let imageView = NSImageView(frame:imageFrame)
 
        //C.
        let newImage = image.tintedImageWithColor(NSColor.randomColor())
 
        //D.
        imageView.image = newImage
        targetLayer.addSubview(imageView)
      }
    }
  }
}

The above code does the following:

  1. Only responds to the known sparkle action
  2. Loads a star image from the bundle
  3. Makes some copies of the star image and…
    1. Generates some random numbers to alter the star position.
    2. Creates an NSImageView and sets its frame.
    3. Gives the image a random color — unless you’re going for a David Bowie tribute, black stars are a bit gothic.
    4. Places the image on the view.

Build and run. Now you can drag from the sparkles view onto the sticker view to add a spray of stars to your view.

final

Where to go From Here?

Congratulations, you created a custom drag and drop interface in your very own app!

You can use the Save Image To Desktop button to save your image as a JPG with the name StickerDrag. Maybe take it a step further and tweet it to the team @rwenderlich.

Here’s the source code for the the completed project.

This drag and drop tutorial for macOS covered the basics of the Cocoa drag and drop mechanism, including:

  • Creating a dragging destination and accepting several different types of data
  • Using the dragging session lifecycle to provide user feedback of the drag operation
  • Decoding information from the pasteboard
  • Creating a dragging source and providing deferred data
  • Creating a dragging source that provides a custom data type

Now you have the knowledge and experience needed to support drag and drop in any macOS app.

There’s certainly more to learn.

You could study up on how to apply effects, such as changing the dragging image during the drag or implementing an animated drop transition, or working with promised files — Photos is one application that places promised data on the dragging pasteboard.

Another interesting topic is how to use drag and drop with NSTableView and NSOutlineView, which work in slightly different ways. Learn about it from the following resources:

If you have any questions or comments about this drag and drop tutorial for macOS, please join the discussion below! And remember, sometimes life is a dragging experience, but everything’s better with unicorns and sparkles. :]

The post Drag and Drop Tutorial for macOS appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>