0

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).

flanker
  • 3,840
  • 1
  • 12
  • 20

1 Answers1

1

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)

Alexander
  • 59,041
  • 12
  • 98
  • 151
  • I had a horrible feeling that would be the answer I got! Those APIs really feel like the belong to iOS5 and ObjC, not iOS11+ and Swift. Every year I hope they'll get a rewrite, especially now all the diffable stuff is based on `Identifiable`. Maybe WWDC22... I'll give it a while to see if anyone comes up with a silver bullet, but if not I'll (reluctantly ) accept your answer. – flanker Nov 09 '21 at 16:18
  • 1
    In fairness, I think that even with structure available today, reference types are still pretty appropriate here. Even with identifiable, there’s still reference semantics going on, it just happens to be an I’d used to key into a dictionary, instead of a memory address used to key into RAM. – Alexander Nov 12 '21 at 00:46
  • I could agree ... if it wasn't for diffable data sources, the future of UIKit table/collection views, being based on `Identifiable` rather than reference identify, which is directly at odds with this, and makes it far harder to merge the two sets of APIs in one tableView – flanker Nov 13 '21 at 00:26
  • True. I haven't been able to adopt those yet because of their version requirements, I'm afraid – Alexander Nov 13 '21 at 00:33
  • I've been fighting it all week, trying to retrofit into an older code base, and it's been painful. It also seems to require the underlying objects conform to `NSSecureCoding` which I wasn't expecting and is another layer of complexity. At least the little grey cells are getting a workout :-) – flanker Nov 13 '21 at 00:39