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?
Drag and drop is the concept of picking up a UI element and dropping it somewhere else. Drag and drop supports the metaphor of your app as a surface to be manipulated and touched and not just a set of buttons to be poked at.
You’re not required to implement drag and drop, but doing so enhances the native feeling of your app and increases integration with iOS as a whole. You should provide drag and drop wherever you’d expect to be able to move an object.
SwiftUI provides first-class support for drag and drop in the List
collection, as well as the API for any view to support drag and drop.
In this tutorial, you’ll build a simple bug-reporting iPad app that helps you track detected bugs and export a list of them in a shareable format. Along the way, you’ll learn how to:
- Use the implicit collection drag and drop API.
- Implement explicit drag and drop in any SwiftUI view.
- Render a view to an image to allow inter-app dragging.
Getting Started
Download the starter material using the Download Materials button at the top or bottom of the tutorial. In the starter folder, locate and open PointyBug.xcodeproj. PointyBug is an app for you to log UI/UX bugs against any app you might be building or testing.
The starter project is a semi-complete app to which you’ll add drag and drop actions. To keep you focused on the drag and drop elements, the CRUD logic is complete and resides in a class called BugController
.
BugController
manages a model array of Bug
objects and associated DisplayElement
objects. The BugController
class conforms to @ObservableObject
and broadcasts model changes to the SwiftUI view stack. A single BugController
is instantiated in AppMain
and injected into the view hierarchy as an @EnvironmentObject
, allowing all the views to access the instance.
Select the Simulator iPad Pro (9.7-inch) in the active scheme settings:
Build and run. Then, rotate the simulated device to landscape. You can see a Master-Detail split view:
Tap Add Bug to add a new bug to the list. You can edit the description text, but tapping the photo button in the navigation bar doesn’t do anything. That seems like a bug!
Take a screenshot by selecting Device ▸ Trigger Screenshot from the menu. You’ll use that image later in the tutorial.
Now, you’ll learn how to add drag and drop to a SwiftUI collection view.
Adding Drop Support to a Collection
SwiftUI has a List
collection view. You use ForEach
in a List
to iterate over a collection of data items. ForEach
conforms to DynamicViewContent
which provides three methods to deal with drop, move and delete operations.
In this section, you’ll update the master List
to accept images and text from any dragging source.
In the Project navigator, inside the PointyBug/Views group, open BugListView.swift. Next, open the SwiftUI Preview Canvas with the key combination Command-Option-Return. Resume the preview with the key combination Command-Option-P. You have a List
and a Button
at the bottom of the screen. Each item in the list is a NavigationLink
that opens an EditorView
.
In BugListView.swift inside List
, add this modifier to the closing brace of ForEach
:
.onInsert(of: [.plainText, .image]) { index, itemProviders in
// insert here later
}
You can make it easier to see where to add the code by “folding” the ForEach
statement. To do this, click shaded area to the left of the line containing ForEach
. Or, place your cursor anywhere inside the ForEach
closure and select Editor ▸ Code Folding ▸ Fold from the menu.
Although the word drop isn’t mentioned, you told List
to accept pasted content of UTType.image
and UTType.plainText
. When the content is dropped, you run a closure with parameters of index
that describe where in the list to put the content and itemProviders
that serves as an array of NSItemProvider
objects.
What are these UTType
types? Before you finish your list drop implementation, you’ll go down a side road for a while.
What Are Uniform Type Identifiers?
A Uniform Type Identifier (UTI) is a way of describing the types of data your system uses. UTI’s are a class hierarchy describing data formats:
By using UTType.image
, you told your list to accept any pasteboard content that looks like image data, such as a JPEG (UTType.jpeg
) or a PNG (UTType.png
). Both these types conform to UTType.image
. There’s a UTType
declared for most data types you’ll encounter during iOS development.
Later in the tutorial, you’ll create a new UTType
for private use in the app.
Decoding Dropped Data
In this section, you’ll find out how to extract the dropped data and add that data to your model. The array of NSItemProvider
objects wraps the data that’s passed to the closure in onInsert
via the itemProviders
parameter.
The first thing to do is create an object to deal with this unwrapping task. You never want to place model management logic inside your SwiftUI views.
Create a new Swift file called ContentDropController.swift in the Controller group.
First, define the class by placing this declaration at the top of ContentDropController.swift:
import SwiftUI
class ContentDropController {
let bugController: BugController
let bugID: BugID?
init(bugID: BugID?, bugController: BugController) {
self.bugID = bugID
self.bugController = bugController
}
}
ContentDropController
talks to BugController
. You want to know about the specific bug you’re updating, so you pass the bugID
.
Next, you’ll create some utility methods that unwrap an NSItemProvider
and ask BugController
to create or update an existing bug.
Now, add this extension
to the end of the file:
extension ContentDropController {
private func unwrapImage(
from provider: NSItemProvider,
completion: @escaping (UIImage?) -> Void
) {
_ = provider.loadObject(ofClass: UIImage.self) { image, error in
var unwrappedImage: UIImage?
defer {
completion(unwrappedImage)
}
if let error = error {
print("image drop failed -", error.localizedDescription)
} else {
unwrappedImage = image as? UIImage
}
}
}
}
In unwrapImage(from:completion:)
, you ask an NSItemProvider
that holds a UIImage
to load that image. You then call completion
with that optional image.
Next, add this method to the same extension:
private func createBugWithImage(
from provider: NSItemProvider,
at dropIndex: Int
) {
// 1
unwrapImage(from: provider) { image in
if let image = image {
// 2
let imageName = ImageController.store(image)
// 3
var bug = self.bugController.createBug()
bug.imageName = imageName
// 4
DispatchQueue.main.async {
self.bugController.insert(bug, at: dropIndex)
}
}
}
}
createBugWithImage(from:at:)
creates a bug that has an image:
- First, you use
unwrapImage(from:completion:)
to get the image fromNSItemProvider
. - Store
image
asData
in the app documents folder and receive the storage name back. - Create a new
Bug
and set the image’s name. - Ask
BugController
to add the new bug to the model list at the requested index. Because the drop request occurs off the main queue, you need to dispatch the call toBugController
back to the main queue.
Finally, add these two methods to the extension:
private func updateBugWithImage(
from provider: NSItemProvider,
at dropIndex: Int
) {
guard
let bugID = bugID,
let selectedBug = self.bugController.bug(withID: bugID)
else {
return
}
unwrapImage(from: provider) { image in
guard
let image = image,
let imageName = ImageController.store(image)
else {
return
}
var selectedBug = selectedBug
selectedBug.imageName = imageName
DispatchQueue.main.async {
self.bugController.update(selectedBug)
}
}
}
updateBugWithImage(from:at:)
handles the case where you’re adding an image to a bug that already exists. You also store the dropped image, but instead of creating a new bug, you update an existing one.
Finally, add the following method to the extension:
private func createBugWithString(
from provider: NSItemProvider,
at dropIndex: Int
) {
_ = provider.loadObject(ofClass: String.self) { text, _ in
var newBug = self.bugController.createBug()
newBug.text = text ?? ""
DispatchQueue.main.async {
self.bugController.insert(newBug, at: dropIndex)
}
}
}
createBugWithString(from:at:)
is like createBugWithImage(from:at:)
. You ask an NSItemProvider
that holds a String
to load that string. Then you create a new bug with that string.
You now have a way to create and update bugs with dropped images. Next, you’ll add a way to handle a list of different item providers and create or update bugs accordingly.