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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Drag and Drop Tutorial for SwiftUI
35 mins
- Getting Started
- Adding Drop Support to a Collection
- What Are Uniform Type Identifiers?
- Decoding Dropped Data
- Creating a Bug With Image
- Updating a Bug and Creating a Bug with Text
- Receiving Dropped Data
- Finishing the Drop
- Adding a Bug
- Adding Reorder and Delete Support
- Adding Drop Support to any View
- Adding Image Drop Support
- Dragging Within the App
- Creating Your Own UTType
- Exporting Your Own UTType
- Receiving a Custom UTType
- Adding Display Element to Image
- Receiving a Drop
- Dragging Content Outside the App
- Rendering Content with UIGraphicsImageRenderer
- Creating an NSItemProvider
- Dragging That Thing
- Where to Go From Here?
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.
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
:
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:
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:
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.
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: