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