Drag and Drop in LazyVGrid : SwiftUI

DevTechie Inc
May 11, 2023

iOS 15 introduced onDrag, which works with onDrop to provide a drag-and-drop functionality.

Here is how our final product will look like.

We will begin with a data structure for the grid.

struct Grid: Identifiable {
    var id = UUID().uuidString
    var iconName: String
    var icon: Image {
        Image(systemName: iconName)
    }
    var backgroundColor = Color.random
}

We will add an extension to color to generate random colors.

extension Color {
    static var random: Color {
        return Color(
            red: Double.random(in: 0...1),
            green: Double.random(in: 0...1),
            blue: Double.random(in: 0...1))
    }
}

Let’s also create a view model which will publish grid items. ViewModel will also keep track of current grid item.

class GridViewModel: ObservableObject{
    
    @Published var currentGrid: Grid?
    
    @Published var gridItems = [
        Grid(iconName: "scooter"),
        Grid(iconName: "bicycle"),
        Grid(iconName: "box.truck"),
        Grid(iconName: "ferry"),
        Grid(iconName: "cablecar"),
        Grid(iconName: "tram"),
        Grid(iconName: "bolt.car"),
        Grid(iconName: "car"),
        Grid(iconName: "airplane"),
        Grid(iconName: "bus.fill"),
        Grid(iconName: "sailboat"),
    ]
    
}

We will create a new struct conforming to the DropDelegate protocol.

struct DropViewDelegate: DropDelegate

DropDelegate requires three functions to be implemented.

performDrop: which tells the delegate that it can ask for the item provider data from the given

dropEntered : Tells the delegate that a validated drop has entered the modified view.

dropUpdated : Tells the delegate that a validated drop is moved inside the modified view.

struct DropViewDelegate: DropDelegate {
    
    var grid: Grid
    var gridData: GridViewModel
    
    func performDrop(info: DropInfo) -> Bool {
        return true
    }
    
    func dropEntered(info: DropInfo) {let fromIndex = gridData.gridItems.firstIndex { (grid) -> Bool in
            return grid.id == gridData.currentGrid?.id
        } ?? 0
        
        let toIndex = gridData.gridItems.firstIndex { (grid) -> Bool in
            return grid.id == self.grid.id
        } ?? 0
        
        if fromIndex != toIndex{
            withAnimation(.default){
                let fromGrid = gridData.gridItems[fromIndex]
                gridData.gridItems[fromIndex] = gridData.gridItems[toIndex]
                gridData.gridItems[toIndex] = fromGrid
            }
        }
    }
    
    func dropUpdated(info: DropInfo) -> DropProposal? {
        return DropProposal(operation: .move)
    }
}

Last but not the least, we will use it inside our view.

struct DevTechieGridDragDrop: View {
    
    @StateObject var gridData = GridViewModel()
    
    let columns = Array(repeating: GridItem(.flexible(), spacing: 10), count: 3)
    
    var body: some View {
        NavigationStack {
            VStack{
                ScrollView{
                    LazyVGrid(columns: columns,spacing: 20, content: {
                        ForEach(gridData.gridItems){ grid in
                            ZStack {
                                grid.backgroundColor
                                grid.icon
                                    .font(.system(size: 40))
                                    .foregroundStyle(Color.white.shadow(.drop(radius: 5)))
                            }
                            .frame(height: 150)
                            .cornerRadius(15)
                            .onDrag({
                                gridData.currentGrid = grid
                                return NSItemProvider(object: String(grid.iconName) as NSString)
                            })
                            .onDrop(of: [.text], delegate: DropViewDelegate(grid: grid, gridData: gridData))
                        }
                    })
                    .padding()
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .navigationTitle("DevTechie")
        }
    }
}

The complete code looks like this:

struct Grid: Identifiable {
    var id = UUID().uuidString
    var iconName: String
    var icon: Image {
        Image(systemName: iconName)
    }
    var backgroundColor = Color.random
}
class GridViewModel: ObservableObject{
    
    @Published var currentGrid: Grid?
    
    @Published var gridItems = [
        Grid(iconName: "scooter"),
        Grid(iconName: "bicycle"),
        Grid(iconName: "box.truck"),
        Grid(iconName: "ferry"),
        Grid(iconName: "cablecar"),
        Grid(iconName: "tram"),
        Grid(iconName: "bolt.car"),
        Grid(iconName: "car"),
        Grid(iconName: "airplane"),
        Grid(iconName: "bus.fill"),
        Grid(iconName: "sailboat"),
    ]
    
}
struct DropViewDelegate: DropDelegate {
    
    var grid: Grid
    var gridData: GridViewModel
    
    func performDrop(info: DropInfo) -> Bool {
        return true
    }
    
    func dropEntered(info: DropInfo) {
        let fromIndex = gridData.gridItems.firstIndex { (grid) -> Bool in
            return grid.id == gridData.currentGrid?.id
        } ?? 0
        
        let toIndex = gridData.gridItems.firstIndex { (grid) -> Bool in
            return grid.id == self.grid.id
        } ?? 0
        
        if fromIndex != toIndex{
            withAnimation(.default){
                let fromGrid = gridData.gridItems[fromIndex]
                gridData.gridItems[fromIndex] = gridData.gridItems[toIndex]
                gridData.gridItems[toIndex] = fromGrid
            }
        }
    }
    
    func dropUpdated(info: DropInfo) -> DropProposal? {
        return DropProposal(operation: .move)
    }
}
struct DevTechieGridDragDrop: View {
    
    @StateObject var gridData = GridViewModel()
    
    let columns = Array(repeating: GridItem(.flexible(), spacing: 10), count: 3)
    
    var body: some View {
        NavigationStack {
            VStack{
                ScrollView{
                    LazyVGrid(columns: columns,spacing: 20, content: {
                        ForEach(gridData.gridItems){ grid in
                            ZStack {
                                grid.backgroundColor
                                grid.icon
                                    .font(.system(size: 40))
                                    .foregroundStyle(Color.white.shadow(.drop(radius: 5)))
                            }
                            .frame(height: 150)
                            .cornerRadius(15)
                            .onDrag({
                                gridData.currentGrid = grid
                                return NSItemProvider(object: String(grid.iconName) as NSString)
                            })
                            .onDrop(of: [.text], delegate: DropViewDelegate(grid: grid, gridData: gridData))
                        }
                    })
                    .padding()
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .navigationTitle("DevTechie")
        }
    }
}
extension Color {
    static var random: Color {
        return Color(
            red: Double.random(in: 0...1),
            green: Double.random(in: 0...1),
            blue: Double.random(in: 0...1))
    }
}