How to implement custom type for onDrag and onInsert

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?

Answered by LogicalLight in 683823022

Answering my own question.

  • .onInsert only works with List. Use onDrop 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.
Accepted Answer

Answering my own question.

  • .onInsert only works with List. Use onDrop 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.
How to implement custom type for onDrag and onInsert
 
 
Q