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