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)