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).
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
Objective C Nsstring* Property Retain Count Oddity
How to Use Generic Protocol as a Variable Type
Why Uitableviewautomaticdimension Not Working
Swiftui - How to Pass Environmentobject into View Model
Swift Nsdateformatter Not Working
How to Programmatically Connect to a Wifi Network Given the Ssid and Password
Swift Convert Unix Time to Date and Time
How to Disable/Enable the Return Key in a Uitextfield
Positioning Mkmapview to Show Multiple Annotations at Once
Detect Permission of Camera in iOS
How to Use an .A Static Library in Swift
Nsurlsession With Nsblockoperation and Queues
Nsphotolibraryusagedescription Key Must Be Present in Info.Plist to Use Camera Roll
Retrieving Carrier Name from iPhone Programmatically
Understanding Performseguewithidentifier
Why Nsdateformatter Is Returning Null for a 19/10/2014 in a Brazilian Time Zone