Search code examples
iosdrag-and-dropios11uidragitem

Drag and Drop asynchronous data fetching


I am trying to implement Drag and Drop into my application which shares images.

All my images are high performance thumbnails (i.e. small size) so I can't use them as my UIDragItem, at least not as the final image.

What I am looking for is a way of providing the URL for my original image and send that off as the UIDragItem and then have the destination fetch the image asynchronously. This is done in the Photos app when an image is stored in iCloud, so it must somehow be possible, I just cant seem to figure out how.


Solution

  • Turns out the solution is quite simple and is described in session 227 Data Delivery with Drag and Drop during this WWDC.

    You basically make whatever object you want to drag conform to NSItemProviderWriting, and then implement two things.

    NSItemProviderWriting:

    The interface for supporting initialization of an item provider based on an object, used by a source app when providing copied or dragged items.

    Step one

    Implement writableTypeIdentifiersForItemProvider which will give your receiver an idea of what type of object you are providing. This is an array of type identifiers with decreasing fidelity (they describe this well in the video)

    Step two

    Implement loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? which does the heavy lifting, this will get called as the receiver tries to load the object you are providing.

    Example

    You can disregard the specifics of the data fetching below (I am using firebase) but using the native URLSession API would work the same way pretty much.

    extension Media: NSItemProviderWriting {
      //Provide the types you want you are supplying
      static var writableTypeIdentifiersForItemProvider: [String]  {
        return [(kUTTypeImage as String)]
      }
    
    
      func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
        print("Item provider would like to write item from path: \(metadata.path!)")
        guard let path = metadata.path else { return nil }
        //Allow a maximum of ~30mb to be downloaded into memory if images, 1GB if video.
        let maxSize:Int64 = (isVideo ? 1000 : 30) * 1024 * 1024
    
        let storage = Storage.storage().reference(withPath: path)
        let progress = Progress(totalUnitCount: 100)
        var shouldContinue = true
        //When the receiver cancels this block is called where we will set the `shouldContinue` to false to cancel the current task
        progress.cancellationHandler = {
          shouldContinue = false
        }
        let task = storage.getData(maxSize: maxSize) { data, error in
          //Once the data is fetched or we encounter an error, call the completion handler
          completionHandler(data, error)
        }
    
        if !shouldContinue {
          task.cancel()
        }
    
        task.observe(.progress) { snapshot in
          if let p = snapshot.progress {
            progress.completedUnitCount = Int64(p.fractionCompleted * 100)
          }
        }
        task.observe(.success) { snapshot in
          print(snapshot)
        }
        task.observe(.failure) { snapshot in
          print(snapshot)
        }
        return progress
      }
    }
    

    Then in our DragDelegate:

    @available(iOS 11, *)
    extension GridViewDelegateDataSource: UICollectionViewDragDelegate {
      func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
    
        let mediaItem = media[indexPath.item]
        //You can now instantiate an NSItemProvider directly from your object because it conforms to the `NSItemProviderWriting` protocol
        let itemProvider = NSItemProvider(object: mediaItem)
        let dragItem = UIDragItem(itemProvider: itemProvider)
        return [dragItem]
      }
    }