Swiftui | Using Ondrag and Ondrop to Reorder Items Within One Single Lazygrid

SwiftUI | Using onDrag and onDrop to reorder Items within one single LazyGrid?

SwiftUI 2.0

Here is completed simple demo of possible approach (did not tune it much, `cause code growing fast as for demo).

demo

Important points are: a) reordering does not suppose waiting for drop, so should be tracked on the fly; b) to avoid dances with coordinates it is more simple to handle drop by grid item views; c) find what to where move and do this in data model, so SwiftUI animate views by itself.

Tested with Xcode 12b3 / iOS 14

import SwiftUI
import UniformTypeIdentifiers

struct GridData: Identifiable, Equatable {
let id: Int
}

//MARK: - Model

class Model: ObservableObject {
@Published var data: [GridData]

let columns = [
GridItem(.fixed(160)),
GridItem(.fixed(160))
]

init() {
data = Array(repeating: GridData(id: 0), count: 100)
for i in 0.. data[i] = GridData(id: i)
}
}
}

//MARK: - Grid

struct DemoDragRelocateView: View {
@StateObject private var model = Model()

@State private var dragging: GridData?

var body: some View {
ScrollView {
LazyVGrid(columns: model.columns, spacing: 32) {
ForEach(model.data) { d in
GridItemView(d: d)
.overlay(dragging?.id == d.id ? Color.white.opacity(0.8) : Color.clear)
.onDrag {
self.dragging = d
return NSItemProvider(object: String(d.id) as NSString)
}
.onDrop(of: [UTType.text], delegate: DragRelocateDelegate(item: d, listData: $model.data, current: $dragging))
}
}.animation(.default, value: model.data)
}
}
}

struct DragRelocateDelegate: DropDelegate {
let item: GridData
@Binding var listData: [GridData]
@Binding var current: GridData?

func dropEntered(info: DropInfo) {
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 {
self.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: 160, height: 240)
.background(Color.green)
}
}

Edit

Here is how to fix the never disappearing drag item when dropped outside of any grid item:

struct DropOutsideDelegate: DropDelegate { 
@Binding var current: GridData?

func performDrop(info: DropInfo) -> Bool {
current = nil
return true
}
}
struct DemoDragRelocateView: View {
...

var body: some View {
ScrollView {
...
}
.onDrop(of: [UTType.text], delegate: DropOutsideDelegate(current: $dragging))
}
}

SwiftUI: `onDrop` overlay not going away in LazyVGrid?

Actually it is a SwiftUI bug, because in this scenario onDrag is called, but onDrop is NOT (that's wrong from D&D flow perspective).

A possible workaround is to introduce additional in-progress state that will indicate that D&D really started. (Actually I would think about some refactoring to simplify delegate's interface, but for demo it is ok).

Tested with Xcode 13.3 / iOS 15.4

Here are changes:

  @State private var isUpdating = false // in-progress state

// ...

.overlay(dragging?.id == d.id && isUpdating ? // << additional condition
Color.white.opacity(0.8) : Color.clear)

// ...

.onDrop(of: [UTType.text], delegate: DragRelocateDelegate(item: d, listData: $model.data,
current: $dragging, updating: $isUpdating)) // << transfer into delegate

// ...

func dropEntered(info: DropInfo) {
updating = true // << indicate that D&D begins

// ...

func performDrop(info: DropInfo) -> Bool {
self.updating = false // << D&D finished

SwiftUI: allow simultaneous gestures with `onDrag` in view?

The both use same gesture (LongPress) for activation, so there is direct conflict. A possible approach is to make onDrag conditional (toggled by some external command).

The Draggable/canDrag can be similar as Droppable/acceptDrop in https://stackoverflow.com/a/61081772/12299030.



Related Topics



Leave a reply



Submit