Clifton Labrum
Clifton Labrum

Reputation: 14158

How to Pass More than Just a String in NSItemProvider in SwiftUI Drag and Drop

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)?

Upvotes: 7

Views: 5791

Answers (2)

Mark
Mark

Reputation: 6977

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!

Upvotes: 3

Clifton Labrum
Clifton Labrum

Reputation: 14158

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.

Upvotes: 6

Related Questions