I've refined a little the demo code to better show how it works.
struct ContentView: View {
struct Item: Identifiable {
let id = UUID()
var pos: Int
var value: String
var color: Color
var onMove: Bool = false
}
@State var items : [Item] = [
Item(pos: 0, value: "A", color: .blue),
Item(pos: 1, value: "B", color: .red),
Item(pos: 2, value: "C", color: .blue),
Item(pos: 3, value: "D", color: .red),
Item(pos: 4, value: "E", color: .blue),
Item(pos: 5, value: "F", color: .red),
Item(pos: 6, value: "G", color: .blue),
Item(pos: 7, value: "H", color: .red),
Item(pos: 8, value: "I", color: .blue),
Item(pos: 9, value: "J", color: .red),
Item(pos: 10, value: "K", color: .blue)
] // enough values to activate scroll
@GestureState private var isDetectingLongPress = false
@State private var completedLongPress = false
@State private var activeLongPress = false
@State private var itemOnMove = -1 // What item is being move ? -1 if none
@State private var movingToPos = -1 // What position is it being move ? -1 if none
var longPress: some Gesture {
LongPressGesture(minimumDuration: 0.5) // LongPress to move, shortpress to scroll
.updating($isDetectingLongPress) { currentState, gestureState, transaction in
gestureState = currentState
activeLongPress = true
}
.onEnded { finished in // Only if there was no drag
self.activeLongPress = !finished
}
}
var msg : String {
if itemOnMove >= 0 && itemOnMove <= 10 {
if itemOnMove == movingToPos {
return "Return to position \(movingToPos+1)" // +1 to start at 1
} else {
return "\(items[itemOnMove].value) On move to position \(movingToPos+1)"
}
}
return " "
}
var body: some View {
VStack {
Text("\(msg)")
ScrollView(.horizontal) {
HStack(spacing: 5) {
ForEach(items) { item in
Rectangle()
.fill(item.onMove ? .green : item.color)
.frame(width:40, height:40)
.border(Color.yellow, width: item.pos == movingToPos ? 3 : 0)
.overlay {
Text("\(item.value)")
}
.gesture(
DragGesture(minimumDistance: 20) // Need large enough to move to start drag ; otherwise, allow scroll
.onChanged { value in
items[item.pos].onMove = true
itemOnMove = item.pos
var shift = 0
if value.translation.width > 0 {
shift = Int(round(value.translation.width / 45)) // 40 width + 5 interspace
} else { // round on negative is too small
shift = Int(value.translation.width / 45)
}
movingToPos = item.pos + shift
}
.onEnded { value in
// Need to drag beyond middle of next to effectively move
var shift = 0
if value.translation.width > 0 {
shift = Int(round(value.translation.width / 45))
} else { // le round du négatif est trop petit
shift = Int(value.translation.width / 45)
}
let newPos = item.pos + shift
if newPos == item.pos { // No change
items[item.pos].onMove = false
} else if newPos >= 0 && newPos <= 10 {
let element = items.remove(at: item.pos)
items.insert(element, at: newPos)
items[newPos].onMove = false
for itemPos in 0...10 {
items[itemPos].pos = itemPos
}
}
itemOnMove = -1
movingToPos = -1
}
)
.gesture(longPress)
}
}
}
.scrollDisabled(activeLongPress)
}
}
}