There was a few additional issues raised to the excellent solutions above, so here's what I could come up with on Jan 1st with a hangover (i.e. apologies for being less than eloquent):
- If you pick a griditem and release it (to cancel), then the view is not reset
I added a bool that checks if the view had been dragged yet, and if it hasn't then it doesn't hide the view in the first place. It's a bit of a hack, because it doesn't really reset, it just postpones hiding the view until it knows that you want to drag it. I.e. if you drag really fast, you can see the view briefly before it's hidden.
- If you drop a griditem outside the view, then the view is not reset
This one was partially addressed already, by adding the dropOutside delegate, but SwiftUI doesn't trigger it unless you have a background view (like a color), which I think caused some confusion. I therefore added a background in grey to illustrate how to properly trigger it.
Hope this helps anyone:
import SwiftUI
import UniformTypeIdentifiers
struct GridData: Identifiable, Equatable {
let id: String
}
//MARK: - Model
class Model: ObservableObject {
@Published var data: [GridData]
let columns = [
GridItem(.flexible(minimum: 60, maximum: 60))
]
init() {
data = Array(repeating: GridData(id: "0"), count: 50)
for i in 0..<data.count {
data[i] = GridData(id: String("\(i)"))
}
}
}
//MARK: - Grid
struct DemoDragRelocateView: View {
@StateObject private var model = Model()
@State private var dragging: GridData? // I can't reset this when user drops view ins ame location as drag started
@State private var changedView: Bool = false
var body: some View {
VStack {
ScrollView(.vertical) {
LazyVGrid(columns: model.columns, spacing: 5) {
ForEach(model.data) { d in
GridItemView(d: d)
.opacity(dragging?.id == d.id && changedView ? 0 : 1)
.onDrag {
self.dragging = d
changedView = false
return NSItemProvider(object: String(d.id) as NSString)
}
.onDrop(of: [UTType.text], delegate: DragRelocateDelegate(item: d, listData: $model.data, current: $dragging, changedView: $changedView))
}
}.animation(.default, value: model.data)
}
}
.frame(maxWidth:.infinity, maxHeight: .infinity)
.background(Color.gray.edgesIgnoringSafeArea(.all))
.onDrop(of: [UTType.text], delegate: DropOutsideDelegate(current: $dragging, changedView: $changedView))
}
}
struct DragRelocateDelegate: DropDelegate {
let item: GridData
@Binding var listData: [GridData]
@Binding var current: GridData?
@Binding var changedView: Bool
func dropEntered(info: DropInfo) {
if current == nil { current = item }
changedView = true
if item != current {
let from = listData.firstIndex(of: current!)!
let to = listData.firstIndex(of: item)!
if listData[to].id != current!.id {
listData.move(fromOffsets: IndexSet(integer: from),
toOffset: to > from ? to + 1 : to)
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
changedView = false
self.current = nil
return true
}
}
struct DropOutsideDelegate: DropDelegate {
@Binding var current: GridData?
@Binding var changedView: Bool
func dropEntered(info: DropInfo) {
changedView = true
}
func performDrop(info: DropInfo) -> Bool {
changedView = false
current = nil
return true
}
}
//MARK: - GridItem
struct GridItemView: View {
var d: GridData
var body: some View {
VStack {
Text(String(d.id))
.font(.headline)
.foregroundColor(.white)
}
.frame(width: 60, height: 60)
.background(Circle().fill(Color.green))
}
}