Advanced Drag&Drop in SwiftUI

Umut SERIFLER
8 min readFeb 3, 2023

--

Moving content from one part of an app to another, or from one app to another app is provided by Drag&Drop event handling.

Supports: iOS, iPadOS, Mac Catalyst 16.0+ / macOS 13+

Previously, I explained how to use simple Drag&Drop functions by using Transferable protocol and also showed some basic concepts about the system default functions, which we can use without sweating a lot. To start, I recommend checking the Drag&Drop article before reading this more advanced article. Within this post, we’ll see different use cases for different content types.

Let’s shortly summarise some basic concepts:

Transferable

A protocol that describes how a type interacts with transport APIs such as drag and drop or copy and paste.

It is a Swift declarative way how models in an app can be serialised and deserialised for sharing and data transfer. Conforming Transferable protocol requires implementing TransferRepresentation property. TransferRepresentation is used to import and export the item.

There are three important representations by default;

Data Transfer
CodableRepresentation
DataRepresentation
It is recommended to use CodableRepresentation rather than DataRepresentation if a model conforms to Codable.

File Transfer
FileRepresentation
FileRepresentation is used to transfer a model which involves a large amount of data such as video.

Transfer Customisation
ProxyRepresentation

Definition:

“ It is a Swift-first declarative way to describe how our models can be serialized and deserialized for sharing and data transfer “ WWDC Transferable

We have to support sending our models over to a receiver inside our app or even in another application.

To give an example of where and when we want to use it, let’s assume there are two applications and we want to share any kind of data between them via ShareSheet, Drag&Drop, Copy&Paste or using some other app feature.

When we want to send something between two different apps, there is a lot of binary data that goes across. Determining what the data corresponds to is a quite important part. Either it might be text, video, url or data…

It is time to introduce UTType which is a structure representing the type of data that is used for loading, sending or receiving.

Official definition of UTType is:

A structure that represents a type of data to load, send, or receive.

UTType instance may represent any file type on disk, irrelevant hierarchical classification systems, or abstract data types which is not represented on-disk. In brief, when we have an object or data, no matter what it is or its type, and want to make it recognizable while sharing from one part of an application to another or between applications, we define a keyword, UTType , to represent the data or object. Each instance has a unique identifier and helpful properties such as preferredFilenameExtension and preferredMIMEType .

There are two ways of using a UTType instance in the app:
1. Using one of the existing types under Type Properties
2. Defining our own type in the app’s <Info>.plist

Using the first option is simpler and it mostly fits to general purpose of usage.
The second option is a more enhanced way but it requires a few more steps like following the instructions to name the identifier within the defined rule list or making the custom type unique.

It is shown in the code given below:

ScrollView Right List
ScrollView Left List

To use Drag&Drop function with Transferable

extension CellItem: Transferable {
static var transferRepresentation : some TransferRepresentation {
CodableRepresentation(contentType: .customType)
}
}
extension UTType {
static var customType: UTType { UTType(exportedAs: "com.firstExample.identifier") }
}
Simple Drag&Drop using Transferable

NSItemProvider

An item provider for conveying data or a file between processes during drag-and-drop or copy-and-paste activities, or from a host app to an app extension.

Since iOS 11 has been published, item providers play a central role in Drag&Drop, and in Copy&Paste. They are also still important for an app extension.

Before going deep, it is important to be aware that the system uses an internal queue when calling completion blocks for the NSItemProvider class. Using an item provider with Drag&Drop essentially requires updating the UI on the main queue.

DispatchQueue.main.async {
// Work that impacts the user interface.
}

NSItemProvider has multiple convenient initialisation methods, such as:
init?(contentsOf: URL!)
init(contentsOf: URL, contentType: UTType?, openInPlace: Bool, coordinated: Bool, visibility: NSItemProviderRepresentationVisibility)
init(item: NSSecureCoding?, typeIdentifier: String?)
init()
init(object: NSItemProviderWriting)

There are differences among them depending on the aim of use. In the following examples, I will demonstrate the two highlighted ones;

First approach is creating an instance with contentsOf init method:

.onDrag {
NSItemProvider(contentsOf: URL(string: cellItem.name))!
}

For the onDrop method, we pass some parameters which are explained below:

func onDrop(
of supportedContentTypes: [UTType],
isTargeted: Binding<Bool>?,
perform action: @escaping ([NSItemProvider]) -> Bool
) -> some View

supportedContentType: Describes the types of content this view can accept through Drag&Drop. If it is used with unsupported types, isTargeted doesn’t update.
isTargeted: The binding’s value is true when the cursor is inside the area, and false when the cursor is outside.
action: Contains the dropped items, with types specified by supportedContentTypes .

.onDrop(of: [.url],
isTargeted: nil,
perform: { providers in
guard let replaceObjectIndex = objectList.firstIndex(where: { $0.id == cellItem.id }) else {
return true
}
for item in providers {
_ = item.loadObject(ofClass: URL.self) { url, _ in
if let url = url {
DispatchQueue.main.async {
objectList[replaceObjectIndex] = CellItem(name: url.absoluteString)
}
}
}
}
return true
})

How it works:
Within the onDrag function, we provide an NSItemProvider instance using URL. This URL becomes a carrier to pass an important value for us to know which UTType is used and supported by the onDrop function. If the data you want to drag is not compatible with your supportedContentTypes, it shows notAllowed mark on the preview (shown below) and will not process the data as expected.

Not Supported type for Drag&Drop

Within the onDrop function, we firstly find an item’s index in the list to replace with the new data if all other requirements are met. Once the item index is ready, the dropped content type should be defined in order to get meaningful data. To make this transformation, one of the system methods loadObject can be used by providing an object type; hereURL.self . As a final step, with the data that we generated, <itemIndex> — <url>, we replace existing data with new one.

The second approach, which requires more effort due to its complexity, is creating an instance and using a custom type supported by DropDelegate:
There is a list of naming conventions rules you might want to check before going to the next step. After the naming of the custom type is clear, it must be added into the project <Info>.plist file following the hierarchy respectively Project Target → Info -> Exported Type Identifiers.

Custom type definition Info .plist file

Here is a simple object definition:

The onDrag function on the left-side list (Yellow), which is a transmitter list in our example, must embrace the NSItemProvider with an object. We use DraggableItem object as content and it gives a possibility of passing larger size of data.

The onDrop function on the right-side list (Green), which is a receiver list in our example, must be contributed by providing supported types.
Even if there is a simple option, like shown below, this is not suitable for complex operations due to lack of flexibility:

func onDrop(
of supportedContentTypes: [UTType],
isTargeted: Binding<Bool>?,
perform action: @escaping ([NSItemProvider]) -> Bool
) -> some View

The recommended option would be using a custom object, which is DraggableItemDelegate here, conforming to the DropDelegate protocol which provides great methods such as validateDrop, dropEntered, dropUpdate, dropExited which you might need to use in different cases. Even though all are required methods, only dropUpdate doesn’t have a default implementation meaning we have to fill it in. We use the custom type here which we defined in the .plist file as a supported type.

onDrop Function Implementation

A parameter we gives into DraggableItemDelegate is a simple value that we use in the title of the list to show dropped data content.

Custom object conforms to DropDelegate

In the performDrop method, we firstly check whether at least one item conforms to at least one of the specified UTIs (Uniform Type Identifier).

Then, we get an array of items and make type casting in order to turn the object into useful data for us.

Last step is using the data to update UI or save into the memory or in any way we want.

NSItemProviderWriting — NSItemProviderReading

NSItemProviderWriting: The protocol for implementing a class to allow an item provider to retrieve data from an instance of the class. A source app uses an object that conforms to this protocol to initialize an item provider for a copied or dragged item.

NSItemProviderReading: The protocol for implementing a class to allow an item provider to create an instance of the class. A destination app uses an object that conforms to this protocol to consume pasted or dropped items.

NsItemProviderWriting
NSItemProviderReading

Here is the final result DropDelegate + Custom UTType:

Sample of Custom DropDelegate Usage

I hope you enjoyed reading. Please leave a comment if you have any question or suggestion.

--

--

Umut SERIFLER

Professionally enjoying iOS, Android, Vapor, Nest.js development (⌚️📱🖥️🗂️)