To implement row reordering I know I could use List{ForEach{}.onMove}
but in my situation I can't use List
for reasons. So I need to implement my own item reordering using onDrag
and onInsert
.
These modifiers use NSItemProvider
. I think therefore my dragged data type needs to implement NSItemProviderWriting
(and NSItemProviderReading
). I've looked for examples but the closest code I've found is dragging URLs. When I try to implement these protocols in my type I end up with an error in .onInsert
at (NSItemProvider item).loadObject(ofClass:MyType.self)
that says "Instance method 'loadObject(ofClass:completionHandler:)' requires that 'MyType' conform to '_ObjectiveCBridgeable'"
How should I be using .onDrag
and .onInsert
with a custom type?
Answering my own question.
.onInsert
only works withList
. UseonDrop
instead.- The custom type should be declared
final class MyType: NSObject, NSItemProviderWriting, NSItemProviderReading, Codable
Here are some relevant code snippets that I hope help the next wanderer...
import UniformTypeIdentifiers
let MyTypeUTI: String = UTType.data.identifier // Very suspicious - will this allow ANY public.data to be dropped? Bad.
final class MyType: NSObject, NSItemProviderWriting, NSItemProviderReading, Codable {
static var readableTypeIdentifiersForItemProvider: [String] = [MyTypeUTI]
static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Self {
let jsonDecoder = JSONDecoder()
let item = try! jsonDecoder.decode(Self.self, from: data)
return item
}
public enum Oops: Error {
case invalidTypeIdentifier
}
static var writableTypeIdentifiersForItemProvider: [String] = [MyTypeUTI]
func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: 100)
guard typeIdentifier == MyTypeUTI else {
completionHandler(nil, Oops.invalidTypeIdentifier)
return progress
}
do {
let jsonEncoder = JSONEncoder()
let data = try jsonEncoder.encode(self)
completionHandler(data, nil)
}
catch { completionHandler(nil, error) }
return progress
}
...[The rest of MyType here]...
Then to implement drag and drop in your View:
ScrollView {
VStack {
ForEach(...) { item in
ItemView(item)
.onDrag({ NSItemProvider(object: item) })
.onDrop(of: [MyTypeUTI],
delegate: MyTypeDropDelegate(item: item, ...)
Your DropDelegate
might be like this (very rough code):
struct MyTypeDropDelegate: DropDelegate {
let item: MyType
func performDrop(info: DropInfo) -> Bool {
let providers = info.itemProviders(for: [FigureBoxUTI])
guard let provider = providers.first else {
return false
}
provider.loadObject(ofClass: FigureBox.self) { item, error in
guard let box = box as? FigureBox else {
...handle error...
return
}
...handle drop of item...
}
return true
}
}
To be improved:
- I think that MyTypeUTI needs to be a custom string which extends public.data. UTIs are still a bit of a mystery to me since trying to use UTType was causing errors for me.