All Ruby on Rails Node JS Android iOS React Native Frontend

Drag & Drop in iOS 11+

Intro

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 demo app.

Basics

Drag and drop is built around a new concept called interactions. It works similarly to gesture recognisers. You can add drag and drop interactions to the views. The app from which items are dragged is called the source app. The app on which items are dropped is called the destination app. For drag and drop in a single app, the app plays both roles simultaneously. On iPhone, source and destination apps are the same.

Timeline

Drag and drop hasn’t got a straightforward path from lift, drag, possible drop and data transfer. There are multiple scenarios, check the image below to see how many.

Screen Shot 2017-10-17 at 08.08.54

Simple Drop Operation

To perform simple drop operation, just add paste configuration with an array of type identifiers or NSItemProvidersReading and implement func paste(itemProviders: [NSItemProvider])


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 type of items contained by the session can be handled by delegate, in which way it proposes to handle the drop and does so of its own accord.

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 lower level operation for loading an object, you can use item provider in an item which can provide an error when something goes wrong.

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 collection should react to dragged items with UICollectionViewDropIntent. While loading the content of the dragged item, we can ensure placeholders for the UI. Having downloaded the data, the placeholder is altered with that data.

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 is easy to use and customise.

 

Links / Literature:

Photo by +Simple on Unsplash
Subscribe Netguru Codestories
Join over 10.000 specialists

Code stories CTA
READ ALSO FROM iOS
Read also
Need a successful project?
Estimate project or contact us