7

I have reviewed some questions on Stack Overflow about drag and drop reorder with SwiftUI, and this one was particularly helpful: SwiftUI | Using onDrag and onDrop to reorder Items within one single LazyGrid?

I'm looking to expand this functionality where I drag something from one list of items to another in my SwifUI app. Let's say I have a Task list:

//TaskView.swift

ScrollView{
  VStack{
    ForEach(model.tasks, id: \.self){ task in
      Text(task.name)
        .onDrag{
          NSItemProvider(object: String(task.id) as NSString)
        }
    }
  }
}

...and I also have a Project list that I can drag a Task onto to move it to that project:

//ProjectView.swift

ScrollView{
  VStack{
    ForEach(model.projects, id: \.self){ project in
      Text(project.name)
        .onDrop(of: [UTType.text], delegate: ProjectDropDelegate(project: project))
    }
  }
}

The part I'm struggling with is in my ProjectDropDelegate where I'm trying to determine a couple things:

  1. What kind of object is being dropped on me? (it must be a task)
  2. If it's a task, what is its id so I can take action on it? (or, ideally, I'd get the whole Task object to work with)

I can't figure out how to make my NSItemProvider in .onDrag use anything other than a string and still work with my SwiftUI drag/drop functionality. For what it's worth, my Task and Project objects are Core Data classes.

How can I make NSItemProvider contain key-value pairs so I can pass a type identifier string like myapp.task (for #1 above) and an id (for #2)?

Clifton Labrum
  • 13,053
  • 9
  • 65
  • 128
  • Good resources: https://exploringswift.com/blog/creating-a-nsitemprovider-for-custom-model-class-drag-drop-api (explains using `NSItemProviderReading`, `NSItemProviderWriting`), https://swiftui-lab.com/drag-drop-with-swiftui/ (shows using `itemProviders(for:)` in the drop delegate, where you'll want to check for your custom type that you've registered using the methods explained in the first link. – jnpdx Mar 11 '21 at 19:57
  • Does this answer your question https://stackoverflow.com/a/60832686/12299030? – Asperi Mar 11 '21 at 20:14
  • Thanks for your quick responses. I've previously review all of those links and still found myself confused how my `Task` object can behave like an image (TIFF or `UIImage`) which seem to automatically be compatible with `UTType`. The Exploring Swift article may be what I need, but it's a bit complex, so it'll take me some time to digest it. – Clifton Labrum Mar 11 '21 at 20:42

2 Answers2

5

After further investigation, I found a much simpler way to handle all this. I think NSItemProvider is a bit of a red herring if all you need to do is move data from one part of your app to another. Here's how I went about it and it seems to work great.

I alluded to model.tasks when I generated my list of tasks. Here's more about it:

class TaskModel: ObservableObject {
  static let shared = TaskModel()
  
  @Published var tasks = [Task]()
  var draggedTask: Task? //<-- I added this
  //...
}

I added a draggedTask optional to my model then I set it in my onDrag modifier like this:

Text(task.name)
  .onDrag{
    model.draggedTask = task
    NSItemProvider(object: NSString())
  }

I just pass an empty String object to NSItemProvider to satisfy its requirement for dragging something. Then in my ProjectDropDelegate I can have all the stuff I need, included setting a hovered UI state:

import SwiftUI
import UniformTypeIdentifiers

struct ProjectDropDelegate: DropDelegate {
  @Binding var hovered: Bool
  var project: Project?
  var modelTask = TaskModel.shared
  
  //MARK: Check before we start
  func validateDrop(info: DropInfo) -> Bool {
    //Allow the drop to begin with any String set as the NSItemProvider
    return info.hasItemsConforming(to: [UTType.text])
  }
  
  //MARK: Drop UI State
  func dropEntered(info: DropInfo) {
    //Show the hovered state if we have a draggedTask
    hovered = modelTask.draggedTask != nil
  }
  func dropExited(info: DropInfo) {
    hovered = false
  }
  
  //MARK: Drop and Save
  func performDrop(info: DropInfo) -> Bool {
    if let task = modelTask.draggedTask{
      //Save my task using modelTask...
      return true
    }else{
      return false
    }
  }
}

This is much simpler than I was initially making it.

Clifton Labrum
  • 13,053
  • 9
  • 65
  • 128
  • Why do you have the "var project: Project?" in that DropDelegate? It doesn't look like it's doing anything. – lmunck Apr 21 '21 at 19:54
  • I removed the code where it's used, but it's there to save changes to the `task` down in the `performDrop` function (I change the `project` property on the `task` object). – Clifton Labrum Apr 21 '21 at 21:17
  • This is a nice workaround but only works within your own app. – Daniel Dec 22 '21 at 19:39
2

Ran into the same problem. Something like this would work:

NSItemProvider(item: letter as NSString, typeIdentifier: "public.plain-text")

This would not work:

NSItemProvider(item: letter as NSString, typeIdentifier: "com.myapp.mytype")

Bug or feature? Feature!

The reason why the second, custom type identifier does not work is because this UTI has not been declared anywhere (yet).

The solution is to declare your custom type as UTI:

  1. Open your project > target > Info > Exported Type Identifiers
  2. Add an entry for your type:
    • Identifier: com.myapp.mytype
    • Conforms to: public.data

Now this will work (and you can hand in any object that supports NSSecureCoding):

NSItemProvider(item: letter as NSString, typeIdentifier: "com.myapp.mytype")

Build and run your app and reordering your list will now work!

Mark
  • 6,647
  • 1
  • 45
  • 88
  • In Apple documentation they explictly state that you shouldn't use "public" in your typeIdentifier, but some like "com.example.custom-type". – Sergio Ocón-Cárdenas Mar 03 '22 at 12:22
  • @SergioOcón-Cárdenas Thanks for the heads-up! I've updated the answer. – Mark Mar 03 '22 at 16:48
  • I don't know why you haven't been voted up. I struggle for a couple of hours figuring this out, almost had it, but your answer here is crystal clear. Works like a charm! No more dragging files to my nodes! – RopeySim Jun 01 '23 at 21:07