Drag & Drop in iOS 11+

Introduced in iOS 11, designed to be used on iPads between multiple apps, but works also on iPhones within one app. All discussed code can be found in
Basics
Drag and drop
Timeline
Drag and drop

Simple Drop Operation
To perform simple drop operation, just add paste configuration with an array of type identifiers or NSItemProvidersReading and implement
Complex Operations with Delegates
Drag
UIDragInteractionDelegate provides items which conform to NSItemProvidingWriting for drag, a preview for a dragged view, as well as animates items during lift-drop, and finally, monitors the session.
Providing items for drag:
func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] { let provider = NSItemProvider(object: "Hello" as NSString) return [UIDragItem(itemProvider: provider)] }
Drop
UIDropInteractionDelegate
Regulates if
If the delegate can handle a session:
func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool { return session.hasItemsConforming(toTypeIdentifiers: [kUTTypePlainText as String]) }
UIDropProposal
Tells if the drop should be handled as copy, move or should be forbidden, cancel.
func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal { return UIDropProposal(operation: .copy) }
Handle a drop
session.loadObjects is performed in the background thread
func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) { session.loadObjects(ofClass: NSString.self) { item in DispatchQueue.main.async { label.text = item as NSString as String } } }
if you need
for item in session.items { item.itemProvider.loadObject... }
TableView and CollectionView
The collection view has a drop and drag delegate of type UIDropInteractionDelegate and UICollectionViewDropDelegate. The table view delegates in a similar way. Both of them are akin to InteractionDelegates, but they are empowered with a coordinator which can enhance operations on collections. The coordinator can provide destinationIndexPath for the drop or tell how elements in
UICollectionViewDropProposal is similar to UIDropProposal but, additionally, it contains UICollectionViewDropIntent. On top of that, it helps the collection to determine where the dropped items will be placed.
Simple drop
coordinator.session.loadObjects(ofClass: Cat.self) { cats in collectionView.performBatchUpdates { for cat in cats as! [Cat] { self.viewModel.cats.insert(cat, at: destinationIndexPath.row) } collectionView.reloadSections(IndexSet(integer: destinationIndexPath.section)) } }
Determine if this is a local drag
coordinator.session.loadObjects(ofClass: Cat.self) { cats in DispatchQueue.main.async { if coordinator.session.localDragSession != nil, let previousIndex = self.viewModel.cats.index(where: { $0.name == cat.name }) { //Local drag session self.viewModel.cats.swapAt(previousIndex, destinationIndexPath.row) } else { // External drag session self.viewModel.cats.insert(cat, at: destinationIndexPath.row) } collectionView.reloadSections(destinationIndexPath.section) } }
Drop with placeholders
for item in coordinator.items { let placeholder = UICollectionViewDropPlaceholder(insertionIndexPath: destinationIndexPath, reuseIdentifier: PlaceholderCollectionCell.identifier) let placeholderContext = coordinator.drop(item.dragItem, to: placeholder) item.dragItem.itemProvider.loadObject(ofClass: Cat.self) { cat, error in DispatchQueue.main.async { placeholderContext.commitInsertion { insertionIndexPath in self.viewModel.cats.insert(cat as! Cat, at: insertionIndexPath.row) } } } } coordinator.session.progressIndicatorStyle = .none
Worth mentioning:
- collections should not be reloaded when using placeholders,
- localDragSession will be nil if the source app isn’t the same as the destination app.
NSItemProviderReading and NSItemProviderWriting
If you want to drag or drop a custom object, it should conform to the four basic criteria, implement NSItemProviderWriting and NSItemProviderReading protocols, extend NSObject, and be final.
import UIKit import MobileCoreServices enum CatError: Error { case invalidTypeIdentifier } protocol Shareable where Self: NSObject { static var shareIdentifier: String { get } } extension Shareable { static var shareIdentifier: String { let bundle = Bundle.main.bundleIdentifier! let typeString = String(describing: type(of: self)) return "\(bundle).\(typeString)" } } final class Cat: NSObject, Shareable { let name: String let image: UIImage init(name: String, image: UIImage) { self.name = name self.image = image super.init() } var data: Data? { return """ { "name": "\(name)", "image": "\(UIImageJPEGRepresentation(image, 1.0)!.base64EncodedString())" } """.data(using: .utf8) } init(data: Data) { let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any] name = json["name"] as! String let imageData = Data(base64Encoded: json["image"] as! String)! image = UIImage(data: imageData)! super.init() } } // Providing Cat to destinationApp extension Cat: NSItemProviderWriting { // Tell which identifiers are cat will be conforming to static var writableTypeIdentifiersForItemProvider: [String] { // General identifiers are not recommended, specific identifiers are welcome return [Cat.shareIdentifier, kUTTypeImage as String, kUTTypePlainText as String] } // Prepare data for providing to destinationAPP func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? { switch typeIdentifier { case Cat.shareIdentifier: completionHandler(data, nil) case kUTTypeImage as NSString as String: completionHandler(UIImageJPEGRepresentation(image, 1.0), nil) case kUTTypePlainText as NSString as String: completionHandler(name.data(using: .utf8), nil) default: completionHandler(nil, CatError.invalidTypeIdentifier) } // I'm not returning any progress return nil } } // Receiving cat from sourceApp extension Cat: NSItemProviderReading { // Tell which indentifiers cal will be expecting static var readableTypeIdentifiersForItemProvider: [String] { return [Cat.shareIdentifier, kUTTypeImage as String, kUTTypePlainText as String] } static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Cat { switch typeIdentifier { case Cat.shareIdentifier: return Cat(data: data) case kUTTypeImage as NSString as String: return Cat(name: "no name", image: UIImage(data: data)!) case kUTTypePlainText as NSString as String: return Cat(name: String(data: data, encoding: .utf8)!, image: UIImage()) default: throw CatError.invalidTypeIdentifier } } }
Conclusion
Even if an iPad isn't the target device for your apps, it is worth adding intentions to iPhone apps. Drag and Drop
Links / Literature:
- https://developer.apple.com/videos/wwdc2017/ Videos with WWDC 17
- https://github.com/icanswiftabit/DnDShowcase demo repository