53

I was wondering if it is possible to use the View.onDrag and View.onDrop to add drag and drop reordering within one LazyGrid manually?

Though I was able to make every Item draggable using onDrag, I have no idea how to implement the dropping part.

Here is the code I was experimenting with:

import SwiftUI

//MARK: - Data

struct Data: Identifiable {
    let id: Int
}

//MARK: - Model

class Model: ObservableObject {
    @Published var data: [Data]
    
    let columns = [
        GridItem(.fixed(160)),
        GridItem(.fixed(160))
    ]
    
    init() {
        data = Array<Data>(repeating: Data(id: 0), count: 100)
        for i in 0..<data.count {
            data[i] = Data(id: i)
        }
    }
}

//MARK: - Grid

struct ContentView: View {
    @StateObject private var model = Model()
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: model.columns, spacing: 32) {
                ForEach(model.data) { d in
                    ItemView(d: d)
                        .id(d.id)
                        .frame(width: 160, height: 240)
                        .background(Color.green)
                        .onDrag { return NSItemProvider(object: String(d.id) as NSString) }
                }
            }
        }
    }
}

//MARK: - GridItem

struct ItemView: View {
    var d: Data
    
    var body: some View {
        VStack {
            Text(String(d.id))
                .font(.headline)
                .foregroundColor(.white)
        }
    }
}

Thank you!

Kai Zheng
  • 6,640
  • 7
  • 43
  • 66

7 Answers7

95

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.count {
            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))
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 9
    That works quite well, however, the dragging state variable is not reset to ni when you drag it out of the v-grid, so that the overlay is on the original item. When you start dragging, the item from which you drag gets the whitish overlay-colour. when you drag it out of the v-grid (not on another item), and then release, it moves back to the original position. however, the whitish overlay stays the same and dragging is never set to nil. I also tried to make this work with Sections but my first attempt has failed. – domi852 Sep 02 '20 at 06:26
  • 1
    @user3122959 I've added an onDrop as well on the ScrollView itself to catch when the dragging ends outside the grid itself, setting dragging to nil when drop is performed. – jdanthinne Oct 02 '20 at 13:35
  • @jdanthinne Thanks! Looks great that way. – Kai Zheng Oct 16 '20 at 11:33
  • The DropOutside delegate works well if you drop outside the views you've defined, but if you drag an item into, say, the navigation title of a Nav View (easy to do by accident), the onDrop is never called and current is never set to nil, so the faded item just sticks there until you drag something else. – Jim Haungs Oct 26 '20 at 20:11
  • thanks for the solution. Attaching the drop-outside-delegate to the Scroll View works fine. My ScrollView also has a navigation bar and dropping on the bar does't call the perform drop function but I think it's a minor issue and in most cases it works now. – domi852 Oct 31 '20 at 09:08
  • Great solution. I would only suggest to make `DragRelocateDelegate` generic so that it is easy to copy-paste :) – Murlakatam Dec 02 '20 at 13:21
  • 4
    I noticed that when dropping the dragged item on itself (to cancel an accidental drag) it will be invisible because the dragItem does not get set to nil. Thoughts on how I could achieve that? – orion Dec 31 '20 at 03:22
  • Should be `[UTType.text]`, not `[UIType.text]`. – titusmagnus Jan 28 '21 at 01:55
  • 10
    I think it's safe to say that if @Asperi stops answering questions on Stack Overflow, most SwiftUI development would grind to a halt. This is super helpful. Thank you! – Clifton Labrum Mar 02 '21 at 17:29
  • I agree, nice solution, but it didn't save position of reordered page when terminate the app, and it will be tricky trying to use it with documents in directory – ViOS May 28 '21 at 08:16
  • @ViOS you would have to manually update your persisting data if you want it to stay after termination. – Kai Zheng Jun 05 '21 at 07:45
  • I noticed the DropOutsideDelegate is not called if dropping on 'empty' parts of your generic view. To solve it, add `.contentShape(Rectangle())` before `.onDrop(of: [UTType.text], delegate: DropOutsideDelegate(current: $dragging))` – Kazikal Jan 24 '22 at 08:33
  • Thanks! How would I best use this solution when my data is provided by CoreData and FetchResults? – Claes Jan 26 '22 at 12:25
  • @Asperi: Is this animation in a simple list possible. for me it is somehow now working inside list but working if i move content outside list – Abhishek Thapliyal Jun 30 '22 at 12:16
  • Unfortunately i have to use scrollview for same – Abhishek Thapliyal Jun 30 '22 at 12:27
  • This works pretty good, however there are some problems: 1. If I long press the grid view and don't move my finger, dropEntered(info: DropInfo) is not called. 2. onDrag is called twice if I add onTapGesture handler. 3. Drag Preview view is not rendered until I move dragged view (It's not shown on long press) Any ideas? – Levan Karanadze Oct 19 '22 at 09:08
  • anyone solve the problem with navigation view?. – cristian_064 Nov 25 '22 at 03:34
30

Here's my solution (based on Asperi's answer) for those who seek for a generic approach for ForEach where I abstracted the view away:

struct ReorderableForEach<Content: View, Item: Identifiable & Equatable>: View {
    let items: [Item]
    let content: (Item) -> Content
    let moveAction: (IndexSet, Int) -> Void
    
    // A little hack that is needed in order to make view back opaque
    // if the drag and drop hasn't ever changed the position
    // Without this hack the item remains semi-transparent
    @State private var hasChangedLocation: Bool = false

    init(
        items: [Item],
        @ViewBuilder content: @escaping (Item) -> Content,
        moveAction: @escaping (IndexSet, Int) -> Void
    ) {
        self.items = items
        self.content = content
        self.moveAction = moveAction
    }
    
    @State private var draggingItem: Item?
    
    var body: some View {
        ForEach(items) { item in
            content(item)
                .overlay(draggingItem == item && hasChangedLocation ? Color.white.opacity(0.8) : Color.clear)
                .onDrag {
                    draggingItem = item
                    return NSItemProvider(object: "\(item.id)" as NSString)
                }
                .onDrop(
                    of: [UTType.text],
                    delegate: DragRelocateDelegate(
                        item: item,
                        listData: items,
                        current: $draggingItem,
                        hasChangedLocation: $hasChangedLocation
                    ) { from, to in
                        withAnimation {
                            moveAction(from, to)
                        }
                    }
                )
        }
    }
}

The DragRelocateDelegate basically stayed the same, although I made it a bit more generic and safer:

struct DragRelocateDelegate<Item: Equatable>: DropDelegate {
    let item: Item
    var listData: [Item]
    @Binding var current: Item?
    @Binding var hasChangedLocation: Bool

    var moveAction: (IndexSet, Int) -> Void

    func dropEntered(info: DropInfo) {
        guard item != current, let current = current else { return }
        guard let from = listData.firstIndex(of: current), let to = listData.firstIndex(of: item) else { return }
        
        hasChangedLocation = true

        if listData[to] != current {
            moveAction(IndexSet(integer: from), to > from ? to + 1 : to)
        }
    }
    
    func dropUpdated(info: DropInfo) -> DropProposal? {
        DropProposal(operation: .move)
    }
    
    func performDrop(info: DropInfo) -> Bool {
        hasChangedLocation = false
        current = nil
        return true
    }
}

And finally here is the actual usage:

ReorderableForEach(items: itemsArr) { item in
    SomeFancyView(for: item)
} moveAction: { from, to in
    itemsArr.move(fromOffsets: from, toOffset: to)
}
ramzesenok
  • 5,469
  • 4
  • 30
  • 41
  • 2
    This is terrific, thank you so much for posting this! – Danilo Campos Dec 20 '21 at 21:59
  • 1
    This is amazing, thank you! I've built upon your solution to add support for custom drag previews. You can find the code here: https://github.com/danielsaidi/SwiftUIKit/tree/master/Sources/SwiftUIKit/Lists – Daniel Saidi Aug 30 '23 at 09:30
5

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

  1. 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.

  1. 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))
    }
}

lmunck
  • 505
  • 4
  • 12
4

Here is how you implement the on drop part. But remember the ondrop can allow content to be dropped in from outside the app if the data conforms to the UTType. More on UTTypes.

Add the onDrop instance to your lazyVGrid.

           LazyVGrid(columns: model.columns, spacing: 32) {
                ForEach(model.data) { d in
                    ItemView(d: d)
                        .id(d.id)
                        .frame(width: 160, height: 240)
                        .background(Color.green)
                        .onDrag { return NSItemProvider(object: String(d.id) as NSString) }
                }
            }.onDrop(of: ["public.plain-text"], delegate: CardsDropDelegate(listData: $model.data))

Create a DropDelegate to handling dropped content and the drop location with the given view.

struct CardsDropDelegate: DropDelegate {
    @Binding var listData: [MyData]

    func performDrop(info: DropInfo) -> Bool {
        // check if data conforms to UTType
        guard info.hasItemsConforming(to: ["public.plain-text"]) else {
            return false
        }
        let items = info.itemProviders(for: ["public.plain-text"])
        for item in items {
            _ = item.loadObject(ofClass: String.self) { data, _ in
                // idea is to reindex data with dropped view
                let index = Int(data!)
                DispatchQueue.main.async {
                        // id of dropped view
                        print("View Id dropped \(index)")
                }
            }
        }
        return true
    }
}

Also the only real useful parameter of performDrop is info.location a CGPoint of the drop location, Mapping a CGPoint to the view you want to replace seems unreasonable. I would think the OnMove would be a better option and would make moving your data/Views a breeze. I was unsuccessful to get OnMove working within a LazyVGrid.

As LazyVGrid are still in beta and are bound to change. I would abstain from use on more complex tasks.

Mitch
  • 576
  • 5
  • 11
4

Goal: Reordering Items in HStack

I was trying to figure out how to leverage this solution in SwiftUI for macOS when dragging icons to re-order a horizontal set of items. Thanks to @ramzesenok and @Asperi for the overall solution. I added a CGPoint property along with their solution to achieve the desired behavior. See the animation below.

enter image description here

Define the point

 @State private var drugItemLocation: CGPoint?

I used in dropEntered, dropExited, and performDrop DropDelegate functions.


func dropEntered(info: DropInfo) {
    if current == nil {
        current = item
        drugItemLocation = info.location
    }

    guard item != current, 
          let current = current,
          let from = icons.firstIndex(of: current),
          let toIndex = icons.firstIndex(of: item) else { return }

          hasChangedLocation = true
          drugItemLocation = info.location

    if icons[toIndex] != current {
        icons.move(fromOffsets: IndexSet(integer: from), toOffset: toIndex > from ? toIndex + 1 : toIndex)
    }
}

func dropExited(info: DropInfo) {
    drugItemLocation = nil
}

func performDrop(info: DropInfo) -> Bool {
   hasChangedLocation = false
   drugItemLocation = nil
   current = nil
   return true
}

For a full demo, I created a gist using Playgrounds

Jav Solo
  • 576
  • 1
  • 6
  • 15
  • Hi, if I copy the gist into Playgrounds.app; it crashes when I drag an icon. Using MBP 2015 macOS 12.6.4 and playground Version 4.1 (1676.15), is there something to fix? – iPadawan Apr 02 '23 at 15:36
  • In case of someone reads my previous post; It works when using XCode Playground project, but not in the Playgrounds.app. However there is a drag lag in play, is this delay to avoid or make it shorter? – iPadawan Apr 02 '23 at 18:12
3

I came with a bit different approach that works fine on macOS. Instead of using .onDrag and .onDrop Im using .gesture(DragGesture) with a helper class and modifiers.

enter image description here

Here are helper objects (just copy this to the new file):

// Helper class for dragging objects inside LazyVGrid.
// Grid items must be of the same size
final class DraggingManager<Entry: Identifiable>: ObservableObject {
    
    let coordinateSpaceID = UUID()
    
    private var gridDimensions: CGRect = .zero
    private var numberOfColumns = 0
    private var numberOfRows = 0
    private var framesOfEntries = [Int: CGRect]() // Positions of entries views in coordinate space
    
    func setFrameOfEntry(at entryIndex: Int, frame: CGRect) {
        guard draggedEntry == nil else { return }
        framesOfEntries[entryIndex] = frame
    }
    
    var initialEntries: [Entry] = [] {
        didSet {
            entries = initialEntries
            calculateGridDimensions()
        }
    }
    @Published // Currently displayed (while dragging)
    var entries: [Entry]?
    
    var draggedEntry: Entry? { // Detected when dragging starts
        didSet { draggedEntryInitialIndex = initialEntries.firstIndex(where: { $0.id == draggedEntry?.id }) }
    }
    var draggedEntryInitialIndex: Int?
    
    var draggedToIndex: Int? { // Last index where device was dragged to
        didSet {
            guard let draggedToIndex, let draggedEntryInitialIndex, let draggedEntry else { return }
            var newArray = initialEntries
            newArray.remove(at: draggedEntryInitialIndex)
            newArray.insert(draggedEntry, at: draggedToIndex)
            withAnimation {
                entries = newArray
            }
        }
    }

    func indexForPoint(_ point: CGPoint) -> Int {
        let x = max(0, min(Int((point.x - gridDimensions.origin.x) / gridDimensions.size.width), numberOfColumns - 1))
        let y = max(0, min(Int((point.y - gridDimensions.origin.y) / gridDimensions.size.height), numberOfRows - 1))
        return max(0, min(y * numberOfColumns + x, initialEntries.count - 1))
    }

    private func calculateGridDimensions() {
        let allFrames = framesOfEntries.values
        let rows = Dictionary(grouping: allFrames) { frame in
            frame.origin.y
        }
        numberOfRows = rows.count
        numberOfColumns = rows.values.map(\.count).max() ?? 0
        let minX = allFrames.map(\.minX).min() ?? 0
        let maxX = allFrames.map(\.maxX).max() ?? 0
        let minY = allFrames.map(\.minY).min() ?? 0
        let maxY = allFrames.map(\.maxY).max() ?? 0
        let width = (maxX - minX) / CGFloat(numberOfColumns)
        let height = (maxY - minY) / CGFloat(numberOfRows)
        let origin = CGPoint(x: minX, y: minY)
        let size = CGSize(width: width, height: height)
        gridDimensions = CGRect(origin: origin, size: size)
    }
        
}

struct Draggable<Entry: Identifiable>: ViewModifier {
    
    @Binding
    var originalEntries: [Entry]
    let draggingManager: DraggingManager<Entry>
    let entry: Entry

    @ViewBuilder
    func body(content: Content) -> some View {
        if let entryIndex = originalEntries.firstIndex(where: { $0.id == entry.id }) {
            let isBeingDragged = entryIndex == draggingManager.draggedEntryInitialIndex
            let scale: CGFloat = isBeingDragged ? 1.1 : 1.0
            content.background(
                GeometryReader { geometry -> Color in
                    draggingManager.setFrameOfEntry(at: entryIndex, frame: geometry.frame(in: .named(draggingManager.coordinateSpaceID)))
                    return .clear
                }
            )
            .scaleEffect(x: scale, y: scale)
            .gesture(
                dragGesture(
                    draggingManager: draggingManager,
                    entry: entry,
                    originalEntries: $originalEntries
                )
            )
        }
        else {
            content
        }
    }
    
    func dragGesture<Entry: Identifiable>(draggingManager: DraggingManager<Entry>, entry: Entry, originalEntries: Binding<[Entry]>) -> some Gesture {
        DragGesture(coordinateSpace: .named(draggingManager.coordinateSpaceID))
            .onChanged { value in
                // Detect start of dragging
                if draggingManager.draggedEntry?.id != entry.id {
                    withAnimation {
                        draggingManager.initialEntries = originalEntries.wrappedValue
                        draggingManager.draggedEntry = entry
                    }
                }
                
                let point = draggingManager.indexForPoint(value.location)
                if point != draggingManager.draggedToIndex {
                    draggingManager.draggedToIndex = point
                }
            }
            .onEnded { value in
                withAnimation {
                    originalEntries.wrappedValue = draggingManager.entries!
                    draggingManager.entries = nil
                    draggingManager.draggedEntry = nil
                    draggingManager.draggedToIndex = nil
                }
            }
    }

}

extension View {
    // Allows item in LazyVGrid to be dragged between other items.
    func draggable<Entry: Identifiable>(draggingManager: DraggingManager<Entry>, entry: Entry, originalEntries: Binding<[Entry]>) -> some View {
        self.modifier(Draggable(originalEntries: originalEntries, draggingManager: draggingManager, entry: entry))
    }
}

Now to use it in view you have to do few things:

  • Create a draggingManager that is a StateObject

  • Create a var that exposes either real array you are using or temporary array used by draggingManager during dragging.

  • Apply coordinateSpace from draggingManager to the container (LazyVGrid) That way draggingManager only modifies its copy of the array during the process, and you can update the original after dragging is done.

    struct VirtualMachineSettingsDevicesView: View {

      @ObservedObject
      var vmEntity: VMEntity
    
      @StateObject
      private var devicesDraggingManager = DraggingManager<VMDeviceInfo>()
      // Currently displaying devices - different during dragging.
      private var displayedDevices: [VMDeviceInfo] { devicesDraggingManager.entries ?? vmEntity.config.devices }
    
      var body: some View {
          Section("Devices") {
              LazyVGrid(columns: [.init(.adaptive(minimum: 64, maximum: 64))], alignment: .leading, spacing: 20) {
                  Group {
                      ForEach(displayedDevices) { device in
                          Button(action: { configureDevice = device }) {
                              device.label
                                  .draggable(
                                      draggingManager: devicesDraggingManager,
                                      entry: device,
                                      originalEntries: $vmEntity.config.devices
                                  )
                          }
                      }
                      Button(action: { configureNewDevice = true }, label: { Label("Add device", systemImage: "plus") })
                  }
                  .labelStyle(IconLabelStyle())
              }
              .coordinateSpace(name: devicesDraggingManager.coordinateSpaceID)
              .frame(maxWidth: .infinity, maxHeight: .infinity)
              .buttonStyle(.plain)
          }
    

    }

Damian Dudycz
  • 2,622
  • 19
  • 38
  • Also I belief this is better approach for reordering - with DragAndDrop you can work between apps. For example if you drag the item with string outside to text editor it would pass this text there. This is not what we want for just reordering items. – Damian Dudycz Nov 19 '22 at 08:12
  • nice solution, but not suitable for a grid inside a scroll – zslavman Jul 22 '23 at 19:20
0

Really awesome to the answer in the first floor Here it a bit change from me

  1. overlay(dragging?.id == d.id ? Color.white.opacity(0.8) : Color.clear)

if you want to hide the background under the moved item, use code below instead

.opacity(dragging?.id == d.id ? 0 : 1)
  1. if you don't want to display a green plus icon when drag, use this class to replace the answer by Asperi
struct DropOutsideDelegate: DropDelegate { 
    @Binding var current: GridData?  
        
    func performDrop(info: DropInfo) -> Bool {
        current = nil
        return true
    }

    // must add this function to hide the green plus icon when dragging
    func dropUpdated(info: DropInfo) -> DropProposal? {
        return DropProposal(operation: .move)
    }
 }

finally, really thanks to Asperi