Drag and Drop Tutorial for SwiftUI

Learn how to use the drag and drop API in SwiftUI by building your own simple iPadOS and iOS bug reporting app. By Warren Burton.

4.4 (8) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Adding Drop Support to any View

In the previous section, you saw that you can add the methods onInsert, onDelete and onMove to any View that conforms to DynamicViewContent. To add drag and drop to views that don’t represent some sort of collection, you need to use onDrop. In this section, you’ll add image drop support to the editor.

Adding Image Drop Support

Build and run the app again, then press Add Bug. Tap the new bug in the list to select.

Drag and drop tutorial sample project add new bug screen

There’s a placeholder “Drag Image Here” text. You’ll get this feature working now. The first thing to do is upgrade ContentDropController to help with this task.

In the Project navigator, inside the Controller group, open ContentDropController.swift. Add this extension to the end of the file:

extension ContentDropController: DropDelegate {
  func performDrop(info: DropInfo) -> Bool {
    guard info.hasItemsConforming(to: [.image]) else {
      return false
    }
    return receiveDrop(
      dropIndex: 0,
      itemProviders: info.itemProviders(for: [.image]),
      create: false)
  }
}

In this extension, you conform ContentDropController to DropDelegate, which SwiftUI declares. If DropInfo contains an image, you call receiveDrop(dropIndex:itemProviders:create:) with create as false to update an existing Bug with the dropped image.

Next, in the Project navigator, in the Views group, open EditorView.swift. In the body of EditorView, locate PhotoView, then add this modifier on PhotoView:

.onDrop(of: [.image], delegate: ContentDropController(
  bugID: bug.bugID,
  bugController: bugController))

onDrop(of:delegate:) is available for any View. This code tells PhotoView to accept drops of the type UTType.image and to use ContentDropController as the DropDelegate. When you drop an image, SwiftUI calls the DropDelegate protocol method performDrop(info:).

Build and run, then tap Add Bug. Tap the new bug in the list to select, then drag an image from Photos to EditorView:

New image of two cats on a rug dropped

Add a description for your chosen image, and you now have another bug logged. Press Command-Shift-H to save.

You now have a list of several bugs with images. Wouldn’t it be nice if you could point at what the problem is in your image? In the next section, you’ll add the ability to drag a marker to your image.

Dragging Within the App

In this section, you’ll add an icon to EditorView that you can drag onto the image to mark points of interest. First, open EditorView.swift, then add this view to ZStack after the onDrop(of:delegate:) modifier you just added:

VStack {
  Spacer()
  ToolView()
    .padding()
}

Here, you place ToolView over the top of the image. The Spacer pushes ToolView to the bottom of VStack. ToolView is an arrow image with rounded rectangle borders. It’s a simple, pre-baked view to keep you moving in the tutorial.

Build and run. Select a bug with an image, and now you have an arrow icon at the bottom edge of the image in the detail view:

Image window with arrow icon

You’re going to be able to drag that icon onto the image soon. But first, you need to do some prep work.

Creating Your Own UTType

Drop actions need UTType specifications to work and so do drag actions. To support a custom drop action, you’ll need a custom UTType.

Start by creating a new Swift file called Tool.swift inside the Model group. Add this code to the file:

import UniformTypeIdentifiers

protocol Tool {
  static var uti: UTType { get }
  static var name: String { get }
  static var itemProvider: NSItemProvider { get }
}

You’ve described a general tool protocol to allow a Tool to provide three types useful for drags. To drag something, it needs a Universal Type Identifier, a unique name as well as an item provider which will be used to handle the drop.

Next, add this code to the file:

enum ArrowTool: Tool {
  static var name: String = "arrow"
  static var uti = UTType("com.raywenderlich.pointybug.tool.arrow") ?? .data
  static var itemProvider = NSItemProvider(
    item: ArrowTool.name as NSString,
    typeIdentifier: ArrowTool.uti.identifier)
}

ArrowTool adopts Tool. The type identifier of com.raywenderlich.pointybug.tool.arrow is a reverse-coded name unique to the app. You want to be sure no one else uses that identifier.

Exporting Your Own UTType

You need to tell the other apps about this UTI type. Select the PointyBug project in the Project navigator to open the project settings and then select the PointyBug target. In the Info tab, expand Exported Type Identifiers and click + to add a new UTI.

Enter Toolbox item arrow for the description, and com.raywenderlich.pointybug.tool.arrow for the Identifier — the same value you used in your code. Under Conforms To enter public.data. Similarly to Swift protocols, UTIs can conform to other UTIs. Once you’re done, your new UTI should look like the following image:

Adding a new exported universal type identifier in Xcode

You’ve added an Exported UTI to your application’s Info.plist file and told iOS the UTI exists. Without this declaration, you can’t instantiate the UTType.

You’re now ready to set up a drag operation. In the Project navigator in the folder Views, open ArrowToolView.swift.

Add this modifier to the ZStack in the body of ArrowToolView:

.onDrag { ArrowTool.itemProvider }

Like onDrop(of:delegate:), you can call onDrag(_:) on any View. You return the NSItemProvider for the ArrowTool that you created before.

Build and run. Select any bug with an image, then hold and drag the arrow icon.

A draggable SwiftUI view

The icon will lift and gain a plus symbol. Nothing happens when you drop the arrow. The drop part is what you’ll do next.

Receiving a Custom UTType

When you drag the arrow, you place information on the dragging pasteboard. You need to register for that type of information and ask to receive it. The first thing to do is create another controller to perform all the decoding logic and update the model with that information.

Create a new Swift file called ToolDropController.swift in the Controller group. Add this class declaration to the top of the file:

import SwiftUI

class ToolDropController {
  var bugController: BugController
  var bugID: BugID
  var geometry: GeometryProxy
  var imageSize: CGSize

  init(
    bugID: BugID,
    bugController: BugController,
    geometry: GeometryProxy, 
    imageSize: CGSize
  ) {
    self.bugID = bugID
    self.bugController = bugController
    self.geometry = geometry
    self.imageSize = imageSize
  }
}

In this declaration, you create ToolDropController with everything it needs to know to update the model. Next, add this extension to ToolDropController.swift:

extension ToolDropController {
  static func adjustedImageRect(
    geometry: GeometryProxy,
    imageSize: CGSize
  ) -> CGRect {
    let frame = geometry.frame(in: .global)
    let xScale = frame.width / imageSize.width
    let yScale = frame.height / imageSize.height
    let minScale = min(xScale, yScale)
    let finalImageSize = imageSize
      .applying(CGAffineTransform(scaleX: minScale, y: minScale))
    let xOrigin = (frame.width - finalImageSize.width) / 2.0
    let yOrigin = (frame.height - finalImageSize.height) / 2.0
    let origin = CGPoint(x: xOrigin, y: yOrigin)
    let imageRect = CGRect(origin: origin, size: finalImageSize)
    return imageRect
  }
}

This utility method takes two pieces of information: the size of the image and the size of the rectangle it’s placed inside. Next, you figure out how the image will fit in the box by using the least scale value for either axis. Finally, you use that information to return the rectangle that displays the image in its container:

Image of seashore shown in landscape and portrait layouts