Reputation: 14158
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:
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
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:
com.myapp.mytype
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
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