parent
5410f41f82
commit
4d49774ec8
@ -0,0 +1,113 @@ |
||||
// |
||||
// ForEachGridView.swift |
||||
// LeCountdown |
||||
// |
||||
// Created by Laurent Morvillier on 31/01/2023. |
||||
// |
||||
|
||||
import SwiftUI |
||||
import UniformTypeIdentifiers |
||||
|
||||
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) |
||||
} |
||||
} |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
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 |
||||
} |
||||
} |
||||
|
||||
struct GridData : Identifiable, Equatable { |
||||
var id: Int |
||||
|
||||
var stringId: String { return "\(id)"} |
||||
} |
||||
|
||||
struct ForEachGridView_Previews: PreviewProvider { |
||||
|
||||
static var gridData = (0...10).map { GridData(id: $0) } |
||||
|
||||
static let side = 125.0 |
||||
|
||||
static var previews: some View { |
||||
|
||||
LazyVGrid(columns: [GridItem(.fixed(50.0)), GridItem(.fixed(side))], spacing: 10.0) { |
||||
ReorderableForEach(items: gridData) { data in |
||||
Text(data.stringId) |
||||
.frame(width: side, height: side) |
||||
.background(.cyan) |
||||
} moveAction: { from, to in |
||||
gridData.move(fromOffsets: from, toOffset: to) |
||||
} |
||||
} |
||||
|
||||
} |
||||
} |
||||
Loading…
Reference in new issue