Creating a NSItemProvider for custom model class (Drag & Drop API)

For a project that I will start working on this summers, I am thinking about implementing this feature where you can drag and drop one or multiple items on a view (circleView in this tutorial) and download all those items from the web. The idea of using drag and drop is nothing new in iOS and with iOS 11, Apple has introduced a specific API for users to do just this.

This API allows users to drag items within the app or from one app to another. On iPhones you can only drag the items within the app  while on iPad with the help of split view you can drag items from one app to another.

The drag and drop API is extremely easy to use and configuring it takes just few minutes. There are two separate protocols that deal with drag and drop. Also there is a UITableViewDragDelegate and UITableViewDropDelegate for dragging and dropping items between table views or within same table view.

NSItemProvider is a wrapper that creates a UIDragItem. Essentially, if you want to drag and drop items of type Strings, then you first have to wrap it with an itemProvider that will convert the data to NSString and create a UIDragItem. Then when you drop the item, it will convert it from NSString to String. Apple provides ItemProvider for types like UIImage, NSURL, NSString etc

If you are populating a table view with items of custom class then in order to drag and drop these items you have to use a custom NSItemProvider. In order to do this, you need to conform your model class to NSItemProviderReading, NSItemProviderWriting.

final class PodcastModelData: NSObject, Codable, NSItemProviderReading, NSItemProviderWriting {

Make sure to use NSObject and Codable. Codable protocol is a combination of two other protocols – Encodable & Decodable. We will use Codable to convert our object to JSON data and then when we drop it at the destination, it will revert back to the model class object.

final class PodcastModelData: NSObject, Codable, NSItemProviderReading, NSItemProviderWriting {
   
    
  // 1  
    var collectionName : String?
    var feedUrl: String?
    var artworkUrl100: String?
  // 2  
     init(collectionName: String, feedURL: String, artworkURL100: String) {
        self.collectionName = collectionName
        self.feedUrl = feedURL
        self.artworkUrl100 = artworkURL100
    }
   //3 
    static var writableTypeIdentifiersForItemProvider: [String] {
        return [(kUTTypeData) as String]
    }
    
// 4
    func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
        
        
        let progress = Progress(totalUnitCount: 100)
        // 5
        
        do {
            let encoder = JSONEncoder()
            encoder.outputFormatting = .prettyPrinted
            let data = try encoder.encode(self)
            let json = String(data: data, encoding: String.Encoding.utf8)
            progress.completedUnitCount = 100
            completionHandler(data, nil)
        } catch {
     
            completionHandler(nil, error)
        }
        
        return progress
    }

Let’s break down the above code:

  1. Class properties – three variables all of type of strings
  2.  simple initializer.
  3. First method to conform to –  ‘writableTypeIdentifiersForItemProvider‘ method returns an array of type of identifiers as Strings. Again it’s array so you can give them multiple identifiers but make sure that it’s in the order of precedence. We will be using KUTTypeData since we want to send our item as type Data.
  4. The second method to conform to –  in this method you will convert the  object to the type identifier which in our case is KUTTypeData. So we will be converting the object to JSON.
  5. We are simply encoding the class properties to JSON using JSONEncoder(). The progress variable keeps track of the loading / conversion.
  6. As soon as the loading is complete, the completion hander closure is called and the converted data is passed.

Now lets conform to NSItemProviderReading:

// 1
static var readableTypeIdentifiersForItemProvider: [String] {
        return [(kUTTypeData) as String]
    }
// 2
    static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> PodcastModelData {
        let decoder = JSONDecoder()
        do {
            let myJSON = try decoder.decode(podcastModelData.self, from: data)
            return myJSON
        } catch {
            fatalError("Err")
        }
        
    }

Lets break down the above code:

  1. readableTypeIdentiferForItemProvider – again returning array of type identifiers in order of highest fidelity. In this case we are only returning KUTTypeData.
  2. This method creates a new instance of class with the given data and the identifier. In this method we will be using JSONDecoder() to decode the data back to instance of class (podcastModelData). The actual function returns ‘self’ however, it was giving me some issues so I added final infront of the class name and instead of ‘self‘ wrote the class name.

That’s it really! Now you it’s pretty straight forward to create UIDragItems with custom NSItemProvider.

func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
     
        let podcastItem = PodcastModelData(collectionName: collectionName, feedURL: feedUrl, artworkURL100: artworkUrl)
        
        let itemProvider = NSItemProvider(object: podcastItem)
        let dragItem = UIDragItem(itemProvider: itemProvider)
        return [dragItem]
    }

The NSitemProvider constructor requires the object of the class and not the class itself. In this case I am giving it podcastItem. The drag item constructor requires the itemProvider which we created earlier.

Since I am dropping all these elements on a custom circleView, therefore I am using UIDropInteractionDelegate and using this method:

   func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
     session.loadObjects(ofClass: PodcastModelData.self) { (items) in
            if let podcasts = items as? [PodcastModelData] {
                //....
              //Do whatever you want to do with your dropped items here
            }
            
        }
      
    }

If you want to drop it on a table view then you have to use this function:

func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { 
...
}

 

You can get the full source code for this project on my github here. Also check out the app in action here on my Twitter.

Leave a Reply

Your email address will not be published. Required fields are marked *