基于值类型数据对象的UITableView拖拽API集成方案咨询
Great question—this is a super common pain point when working with Swift value types and Apple's Objective-C-powered drag-and-drop system. The core issue is that NSItemProviderReading and NSItemProviderWriting are @objc class protocols, which Swift structs can't conform to directly. But you don't have to rewrite your structs as classes or hack your data model. Here are two clean, non-intrusive approaches that work with your existing value types.
Option 1: Use Data Serialization (Recommended)
This approach skips protocol conformance entirely by serializing your value type to Data, then using custom read/write logic with NSItemProvider. It's lightweight and requires zero changes to your struct definitions.
Step 1: Ensure your struct conforms to Codable
First, make sure your value type supports serialization (most structs can adopt Codable with no extra work):
struct DeliveryItem: Codable { let id: String let title: String // Add your other properties here }
Step 2: Create drag items in your UITableViewDragDelegate
In your drag delegate method, serialize the struct to Data and wrap it in an NSItemProvider:
extension DeliveryViewController: UITableViewDragDelegate { func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { guard let item = dataSource.itemIdentifier(for: indexPath) as? DeliveryItem else { return [] } // Define a custom UTI to identify your data type let customUTI = UTType(exportedAs: "com.yourcompany.DeliveryItem", conformingTo: .data).identifier do { let itemData = try JSONEncoder().encode(item) let itemProvider = NSItemProvider(item: itemData, typeIdentifier: customUTI) // Optional: Register for generic data support for broader compatibility itemProvider.registerObject(itemData, visibility: .all) return [UIDragItem(itemProvider: itemProvider)] } catch { print("Failed to encode delivery item: \(error)") return [] } } }
Step 3: Handle drops in your UITableViewDropDelegate
Decode the Data back to your struct when handling the drop:
extension DeliveryViewController: UITableViewDropDelegate { func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool { let customUTI = UTType(exportedAs: "com.yourcompany.DeliveryItem", conformingTo: .data).identifier return session.hasItemsConforming(toTypeIdentifiers: [customUTI]) } func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { let destinationIndexPath = coordinator.destinationIndexPath ?? IndexPath(row: 0, section: 0) for dropItem in coordinator.items { let provider = dropItem.dragItem.itemProvider let customUTI = UTType(exportedAs: "com.yourcompany.DeliveryItem", conformingTo: .data).identifier provider.loadObject(ofClass: Data.self, forTypeIdentifier: customUTI) { [weak self] data, error in guard let self = self, let itemData = data as? Data, error == nil else { return } do { let droppedItem = try JSONDecoder().decode(DeliveryItem.self, from: itemData) // Update your diffable data source on the main thread DispatchQueue.main.async { var snapshot = self.dataSource.snapshot() snapshot.insertItems([droppedItem], beforeItem: snapshot.itemIdentifiers[destinationIndexPath.row]) self.dataSource.apply(snapshot, animatingDifferences: true) } } catch { print("Failed to decode delivery item: \(error)") } } } } }
Option 2: Lightweight Temporary Wrapper Class
If your struct can't use Codable (e.g., it has non-serializable properties), create a throwaway wrapper class that conforms to the required protocols. This wrapper is only used during drag-and-drop and doesn't affect your core data model.
Step 1: Create the wrapper class
// This class is only used for drag-and-drop operations fileprivate class DragValueWrapper<T>: NSObject, NSItemProviderReading, NSItemProviderWriting where T: Codable { let wrappedValue: T init(value: T) { self.wrappedValue = value super.init() } // MARK: NSItemProviderWriting static var writableTypeIdentifiersForItemProvider: [String] { return [UTType(exportedAs: "com.yourcompany.DragWrapper.\(String(describing: T.self))", conformingTo: .data).identifier] } func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? { do { let data = try JSONEncoder().encode(wrappedValue) completionHandler(data, nil) } catch { completionHandler(nil, error) } return nil } // MARK: NSItemProviderReading static var readableTypeIdentifiersForItemProvider: [String] { return writableTypeIdentifiersForItemProvider } static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Self { let value = try JSONDecoder().decode(T.self, from: data) return self.init(value: value) } }
Step 2: Use the wrapper in your drag delegate
extension DeliveryViewController: UITableViewDragDelegate { func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { guard let item = dataSource.itemIdentifier(for: indexPath) as? DeliveryItem else { return [] } let wrapper = DragValueWrapper(value: item) let itemProvider = NSItemProvider(object: wrapper) return [UIDragItem(itemProvider: itemProvider)] } }
Step 3: Unwrap the value in your drop delegate
extension DeliveryViewController: UITableViewDropDelegate { func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { let destinationIndexPath = coordinator.destinationIndexPath ?? IndexPath(row: 0, section: 0) for dropItem in coordinator.items { dropItem.dragItem.itemProvider.loadObject(ofClass: DragValueWrapper<DeliveryItem>.self) { [weak self] wrapper, error in guard let self = self, let valueWrapper = wrapper as? DragValueWrapper<DeliveryItem>, error == nil else { return } DispatchQueue.main.async { var snapshot = self.dataSource.snapshot() snapshot.insertItems([valueWrapper.wrappedValue], beforeItem: snapshot.itemIdentifiers[destinationIndexPath.row]) self.dataSource.apply(snapshot, animatingDifferences: true) } } } } }
Key Advantages of Both Solutions
- No changes to your existing value types (avoids bugs and regression risks from struct-to-class conversions)
- No modifications to your core data model or diffable data source setup
- Both approaches work seamlessly with UITableView's built-in drag-and-drop delegates
- Option 1 is the most lightweight, with zero extra class overhead
内容的提问来源于stack exchange,提问作者flanker

