A pure SwiftUI structural component that allows for easy drag-and-drop reordering operations. It enables fast, DragGesture based interactions with its elements instead of to the "long press" based one that comes with .onDrag/.draggable. Here it is in action in the upcoming Vis iOS app:
This currently only a ReorderableVStack generated from a collection of identifiable data (similar to this SwiftUI List initializer). More containers are going to come as needed, but feel free to submit an issue or a PR if there is something you'd like to see.
- Specify your own drag handle with the
.dragHandle()modifier - Disable/Enable dragging via the
.dragDisabled(_ dragDisabled: Bool)modifier, which plays nicely with animations (as opposed to adding/removing a.onDrag()modifier)! - Easily customize your drag state via a
isDraggedparameter passed to yourcontentViewBuilder.
This component is distributed as a Swift Package. Simply add the following URL to your package list:
https://github.com/visfitness/reorderable
To add this package to your XCode project, follow these instructions.
Note
All the following sample use the following struct for their data
private struct Sample: Identifiable {
var color: UIColor
var id: Int
var height: CGFloat
init(_ color: UIColor, _ id: Int, _ height: CGFloat) {
self.color = color
self.id = id
self.height = height
}
}struct SimpleExample: View {
@State var data = [
Sample(UIColor.systemBlue, 1, 200),
Sample(UIColor.systemGreen, 2, 100),
Sample(UIColor.systemGray, 3, 300)
]
var body: some View {
ReorderableVStack(data, onMove: { from, to in
withAnimation {
data.move(fromOffsets: IndexSet(integer: from),
toOffset: (to > from) ? to + 1 : to)
}
}) { sample in
RoundedRectangle(cornerRadius: 32, style: .continuous)
.fill(Color(sample.color))
.frame(height: sample.height)
.padding()
}
.padding()
}
}struct SimpleExample: View {
@State var data = [
Sample(UIColor.systemBlue, 1, 200),
Sample(UIColor.systemGreen, 2, 100),
Sample(UIColor.systemGray, 3, 300)
]
var body: some View {
ReorderableVStack(data, onMove: { from, to in
withAnimation {
data.move(fromOffsets: IndexSet(integer: from),
toOffset: (to > from) ? to + 1 : to)
}
}) { sample, isDragged in // <------ Notice the additional `isDragged` parameter
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 32, style: .continuous)
.fill(Color(sample.color))
.frame(height: sample.height)
Image(systemName: "line.3.horizontal")
.foregroundStyle(.secondary)
.padding()
.offset(x: 16)
// This will now be the only place users can drag the view from
.dragHandle() // <------------
}
.scaleEffect(isDragged ? 1.1: 1)
.animation(.easeOut, value: isDragged)
.padding()
}.padding()
}
}Warning
Because this package doesn't rely on SwiftUI's native onDrag, it also doesn't automatically trigger auto-scrolling when users drag the element to the edge of the parent/ancestor ScrollView. To enable this behavior, the autoScrollOnEdges() modifier needs to be applied to the ScrollView.
struct SimpleExample: View {
@State var data = [
Sample(UIColor.systemBlue, 1, 200),
Sample(UIColor.systemGreen, 2, 200),
Sample(UIColor.systemGray, 3, 300),
Sample(UIColor.systemMint, 4, 200),
Sample(UIColor.systemPurple, 5, 300),
Sample(UIColor.orange, 6, 200)
]
var body: some View {
ScrollView {
ReorderableVStack(data, onMove: { from, to in
withAnimation {
data.move(fromOffsets: IndexSet(integer: from),
toOffset: (to > from) ? to + 1 : to)
}
}) { sample in
RoundedRectangle(cornerRadius: 32, style: .continuous)
.fill(Color(sample.color))
.frame(height: sample.height)
.padding()
}.padding()
}.autoScrollOnEdges() // <------- This modifier enables the autoscrolling
}
}