-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathSwiftUIReorderableForEach.swift
164 lines (140 loc) · 5.45 KB
/
SwiftUIReorderableForEach.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import SwiftUI
import UniformTypeIdentifiers
public struct ReorderableForEach<Data, Content>: View
where Data : Hashable, Content : View {
@Binding var data: [Data]
@Binding var allowReordering: Bool
let context: NSManagedObjectContext? // optional, passing context enables CoreData support, requires an attribute 'sortIndex' in CD model
private let content: (Data, Bool) -> Content
@State private var draggedItem: Data?
@State private var hasChangedLocation: Bool = false
public init (_ data: Binding<[Data]>, allowReordering: Binding<Bool>, context: NSManagedObjectContext? = nil, @ViewBuilder content: @escaping (Data, Bool) -> Content) {
_data = data
_allowReordering = allowReordering
self.context = context
self.content = content
}
public var body: some View {
ForEach(data, id: \.self) { item in
if allowReordering {
content(item, hasChangedLocation && draggedItem == item)
.onDrag {
draggedItem = item
return NSItemProvider(object: "\(item.hashValue)" as NSString)
}
.onDrop(of: [UTType.plainText], delegate: DragRelocateDelegate(
item: item,
context: context,
data: $data,
draggedItem: $draggedItem,
hasChangedLocation: $hasChangedLocation))
} else {
content(item, false)
}
}
}
struct DragRelocateDelegate<Data>: DropDelegate
where Data : Equatable {
let item: Data
let context: NSManagedObjectContext?
@Binding var data: [Data]
@Binding var draggedItem: Data?
@Binding var hasChangedLocation: Bool
func dropEntered(info: DropInfo) {
guard item != draggedItem,
let current = draggedItem,
let from = data.firstIndex(of: current),
let to = data.firstIndex(of: item)
else {
return
}
hasChangedLocation = true
if data[to] != current {
// handle UI indices
withAnimation {
data.move(fromOffsets: IndexSet(integer: from),
toOffset: (to > from) ? to + 1 : to)
}
// support for CoreData (rewrite all sortIndex' in order of UI appearance)
if let context = context, let data = data as? [NSManagedObject] {
var index = 0
for preset in data {
preset.setValue(index, forKey: "sortIndex")
index += 1
}
if context.hasChanges {
do { try context.save() }
catch { }
}
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
hasChangedLocation = false
draggedItem = nil
return true
}
}
}
// MARK: tests
struct ReorderingVStackTest: View {
@State private var data = ["Apple", "Orange", "Banana", "Lemon", "Tangerine"]
@State private var allowReordering = false
var body: some View {
VStack {
Toggle("Allow reordering", isOn: $allowReordering)
.frame(width: 200)
.padding(.bottom, 30)
VStack {
ReorderableForEach($data, allowReordering: $allowReordering) { item, isDragged in
Text(item)
.font(.title)
.padding()
.frame(minWidth: 200, minHeight: 50)
.border(Color.blue)
.background(Color.red.opacity(0.9))
.overlay(isDragged ? Color.white.opacity(0.6) : Color.clear)
}
}
}
}
}
struct ReorderingVGridTest: View {
@State private var data = ["Apple", "Orange", "Banana", "Lemon", "Tangerine"]
@State private var allowReordering = false
var body: some View {
VStack {
Toggle("Allow reordering", isOn: $allowReordering)
.frame(width: 200)
.padding(.bottom, 30)
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible())
]) {
ReorderableForEach($data, allowReordering: $allowReordering) { item, isDragged in
Text(item)
.font(.title)
.padding()
.frame(minWidth: 150, minHeight: 50)
.border(Color.blue)
.background(Color.red.opacity(0.9))
.overlay(isDragged ? Color.white.opacity(0.6) : Color.clear)
}
}
}
.padding()
}
}
struct ReorderingVStackTest_Previews: PreviewProvider {
static var previews: some View {
ReorderingVStackTest()
}
}
struct ReorderingGridTest_Previews: PreviewProvider {
static var previews: some View {
ReorderingVGridTest()
}
}