Home > OS >  Using TableView's Drag & Drop API with underlying value type data objects
Using TableView's Drag & Drop API with underlying value type data objects

Time:11-12

In iOS 11 Apple introduced it's drag and drop APIs to the OS (principally to support dragging between apps on iPadOS) via the NSItemProvider class and provided a customised implementation for UITableView with the UITableViewDragDelegate and UITableViewDropDelegate.

Unless I'm missing something (always possible!) when working with diffable data sources for tableViews the best way to use these APIs is to wrap underlying item in the datasource, obtained from the current snapshot, in an NSItemProvider and then embed that in a UIDragItem to facilitate drag and drop via the delegate methods. Specifically in the drag delegate something like:

extension DeliveryViewController :  UITableViewDragDelegate  {
   func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
      let itemType = UTType(exportedAs: "MyDataObject", conformingTo: .item).identifier
      let item = dataSource.itemIdentifier(for: indexPath)!
      let itemProvider = NSItemProvider(item: item, typeIdentifier: itemType)
      return [UIDragItem(itemProvider: itemProvider)]
   }
}

However to utilise the NSItemProvider the underlying data object needs to conform to NSObject, NSItemProviderReading, and NSItemProviderWriting. This is provided for the common NSxxx data types but any custom data objects need to adopt these. Conforming to these protocols is relatively straightforward if your data object is a Class, but not if it's a value type as these are class protocols. Even native Swift value types have to be bridged across to their legacy equivalents (eg. String to NSString, Data to NSData) to be used with these APIs.

Which brings me to the crux of the question: I'm looking to introduce these drag and drop APIs to an established code base where the underlying data objects are all, for good reasons, value types. I'm loathe to change these structs to classes for a number of reasons, including introducing new bugs and regressions.

I've considered (but not yet tested) just wrapping these data objects in class objects in the tableView's view model. Have the wrapper class initialiser take the original struct and add it to a property in the class. Then to conform this wrapper Class to the necessary protocols. This though is an overhead I'd rather avoid if possible as it would mean changing tableView methods and implementing a means to write back any changes to the underlying structs in the wider data model. Plus I'm not yet aware if the if protocols required to support NSItemProvider in a custom class inherit from any other protocols where the value type properties may be problematic.

If anyone has any suggestions on how best to tackle this issue it would be most appreciated (please don't suggest third party libraries as that is not a route I want to pursue).

CodePudding user response:

I've encountered this issue on the Mac side of things (NSTableView), before the diffable data source APIs were introduce.

You're not gonna like hearing this, but I totally recommend you go with classes, 100%.

As you've noticed AppKit/UIKit are very very dependant on the "objects are references with identity" concept the Objective C revolved around. These APIs predate the Identifiable protocol, though it's almost like that protocol implicitly exists. Every NSObject conforms to it, where its identifier is always its object address (object identity).

You might have some structs that make sense to be structs (because they're simple values with value-equality and no identity), but I think things change once you toss them into a table. The moment you do that, Bob Smith on row 4 becomes nonidentical to Bob Smith on row 7, despite being value-equal. Their position within the view gives them a new dimension of identity, that structs can't cope with as-is.

To make matters worse, a lot of these identity-dependant APIs import using Any, rather than AnyObject. If you've imported Foundation, attempting to pass value types to Objective C APIs will make Swift implicitly box them up into _SwiftValue objects.

These objects are hidden away from you, and get created/destroyed behind the scenes. Like any other objects, they have a defined identity based on their address, but that's not something you can access readily, and it's certainly not something you could rely on. (e.g. if you return the same struct value twice, you'll get two copies, each wrapped in a distinct _SwiftValue. The two will be nonidentical)

  • Related