In order to make our filter items draggable, we have quite a few things to do. First, we need to update our PhotoFilterViewController+UIDropInteraction file. Open it up, look for the -dropInteraction:canHandleSession: method, and update the else statement from false to the following:
else { return session.canLoadObjects(ofClass: FilterItem.self) }
When you are done, your method will look like the following:
func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
if session.localDragSession == nil {
return session.canLoadObjects(ofClass: UIImage.self)
}
else { return session.canLoadObjects(ofClass: FilterItem.self) }
}
You can ignore the error for now. Here, we are just making sure that our FilterItem class can be dropped and loaded. Now, we need to do one more update in this class: find the -dropInteraction:performDrop: method. After the if statement we are going to add an else:
else {
for dragItem in session.items {
let itemProvider = dragItem.itemProvider
itemProvider.loadObject(ofClass: FilterItem.self) { (object, error) in
if let error = error {
print(error.localizedDescription)
} else {
DispatchQueue.main.async {
if let item = object as? FilterItem {
print("filter \(item.filter)")
self.filterSelected(item: item)
}
}
}
}
}
}
This else statement we just added will loop through each item that is being dragged and load the object. Our app does not support multiple draggable items, but this is where it is handled. When we add this code we will have another error but we will address this soon. The -dropInteraction:performDrop: method should now look like the following:
func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
if session.localDragSession == nil {
for dragItem in session.items {
loadImage(dragItem.itemProvider)
}
}
else {
for dragItem in session.items {
let itemProvider = dragItem.itemProvider
itemProvider.loadObject(ofClass: FilterItem.self) { (object, error) in
if let error = error {
print(error.localizedDescription)
} else {
DispatchQueue.main.async {
if let item = object as? FilterItem {
print("filter \(item.filter)")
self.filterSelected(item: item)
}
}
}
}
}
}
}
We still have two errors that we need to fix; let's fix those next by opening the FilterItem class inside your Photo Filter folder. Add the following under the init method:
required init(itemProviderData data: Data, typeIdentifier: String) throws {
if typeIdentifier == FilterItem.typeIdentifier {
let item = try? JSONDecoder().decode(FilterItem.self, from: data)
if let i = item {
self.name = i.name
self.filter = i.filter
return
}
}
throw ParseError.decodingFailed("Invalid type!")
}
We are adding our a init method that we need for dragging. This allows us to pass the data around. We have two errors because we need to conform to NSItemProviderReading and NSItemProviderWriting. We will write them both in the same file. In the Photo Filter folder, create a new Swift class and add the first extension:
import UIKit
import MobileCoreServices
extension FilterItem: NSItemProviderReading {
static var readableTypeIdentifiersForItemProvider: [String] {
return [FilterItem.typeIdentifier]
}
static func object(withItemProviderData data: Data,
typeIdentifier: String) throws -> Self {
switch typeIdentifier {
case FilterItem.typeIdentifier:
return try! JSONDecoder().decode(self, from: data)
default:
throw ParseError.decodingFailed("Invalid type!")
}
}
}
This extension is to conform to NSItemProviderReading, which decodes the data that we are passing so that we can read it. Next, add the last extension by adding the following after our first extension:
extension FilterItem: NSItemProviderWriting {
static var writableTypeIdentifiersForItemProvider: [String] {
return [FilterItem.typeIdentifier, kUTTypeUTF8PlainText as String]
}
func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
let data: Data?
switch typeIdentifier {
case FilterItem.typeIdentifier:
data = try? JSONEncoder().encode(self)
case kUTTypeUTF8PlainText as NSString as String:
data = "\(name), \(filter)".data(using: .utf8)
default:
data = nil
}
completionHandler(data, nil)
return nil
}
}
This extension allows us to export our data into a binary representation by first making sure the class type is of FilterItem. We are just about done with all the setup we need. We now need to make sure that our Collection View allows dragging. Currently, if you try to drag, nothing will happen. Open the PhotoFilterViewController and, in the setupCollectionView() method, add the following at the bottom of the method:
collectionView?.dragDelegate = self
Adding this line will show an error, but you can ignore it, as we are about to fix it. When you finish, the entire method will look like the following:
func setupCollectionView() {
let layout = UICollectionViewFlowLayout()
if Device.isPhone { layout.scrollDirection = .horizontal }
else { layout.scrollDirection = .vertical }
layout.sectionInset = UIEdgeInsets(top: 7, left: 7, bottom: 7, right: 7)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 7
collectionView?.collectionViewLayout = layout
collectionView?.dragDelegate = self
}
This now enables dragging from a Collection View, but we now implement the drag delegate code for it all to work. Before we create another extension, let's create a new folder inside the Photo Filter folder and name this folder Extensions. Then drag both files we created earlier, Filter-Item+NSItemProvider and PhotoFilterViewController+UIDropInteraction, into the Extensions folder.
Now that we have that folder set up right, click the Extensions folder inside the Photo Filter folder and create a new file called PhotoFilterViewController+ UICollectionViewDrag. Once you have created the file, add the following:
import UIKit
extension PhotoFilterViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let itemProvider = NSItemProvider(object: data[indexPath.item])
let dragItem = UIDragItem(itemProvider: itemProvider)
return [dragItem]
}
}
This method -collectionView:itemsForBeginningDragSession:atIndexPath: we are getting the data for the item at index path as a drag is being made. If the array is empty, the drag session will not begin. We are finished; if you build and run the project, you will now be able to drag filter items onto the selected photo. Let's drag from our filter list:

After dropping the filter item, you should see the following:
