Die Grundidee:
Elemente werden in einer horizontalen Spur innerhalb einer ScrollView (.horizontal) angeordnet.
Im Bearbeitungsmodus können Sie ein Element horizontal ziehen.
Während des Ziehens aktualisiere ich das Array und Berechnen Sie alle Artikelpositionen neu, um sie live neu anzuordnen.
Der folgende Code ist ein Minimalbeispiel. Funktionell funktioniert es, aber das Ziehen fühlt sich nicht gleichmäßig an:
Das gezogene Element zittert und die anderen Elemente springen herum, während ich ziehe.
Meine Frage ist:
Welche Änderungen sollte ich an dieser Implementierung vornehmen, damit sich die Drag-to-Reorder-Interaktion beim horizontalen Ziehen von Elementen reibungslos und kontinuierlich (kein Zittern) anfühlt?
Automatischer Bildlauf beim horizontalen Ziehen von Zellen in der Nähe der Ränder Scrollende Leinwand
Hier ist der Democode:
Code: Select all
struct ContentView: View {
@State var isEditing: Bool = false
var body: some View {
VStack(alignment: .center, spacing: 30){
Button( isEditing ? "Done" : "Edit", systemImage: isEditing ? "checkmark" : "slider.horizontal.3") {
isEditing.toggle()
}
.buttonStyle(.borderedProminent)
HorizontalCanvasView(editMode: $isEditing)
}
.frame(maxHeight: .infinity)
}
}
struct CanvasItem: Identifiable, Equatable {
let id = UUID()
var color: Color
var size: CGSize = CGSize(width: 100, height: 100)
}
struct HorizontalCanvasView: View {
@State private var items: [CanvasItem] = [
CanvasItem(color: .blue),
CanvasItem(color: .red),
CanvasItem(color: .green)
]
@Binding var editMode: Bool
private let canvasHeight: CGFloat = 300
private let itemSize = CGSize(width: 100, height: 100)
private let itemSpacing: CGFloat = 10
private let horizontalPadding: CGFloat = 200
// Drag state
@State private var draggedID: UUID? = nil
@State private var dragOffsetX: CGFloat = 0 // visual offset for dragged item
@State private var lastTranslationX: CGFloat = 0 // last gesture.translation.width
private var cellWidth: CGFloat {
itemSize.width + itemSpacing
}
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
ZStack(alignment: .topLeading) {
Rectangle()
.fill(Color.gray.opacity(0.1))
.frame(height: canvasHeight)
HStack(spacing: itemSpacing) {
ForEach(items) { item in
CanvasItemView(
item: item,
size: itemSize,
isDragging: draggedID == item.id,
editMode: editMode,
horizontalOffset: draggedID == item.id ? dragOffsetX : 0,
onDragChanged: { value in
handleDragChanged(for: item, value: value)
},
onDragEnded: { value in
handleDragEnded(for: item, value: value)
}
)
}
}
.padding(.horizontal, horizontalPadding)
.frame(height: canvasHeight)
}
}
.frame(height: canvasHeight)
.onChange(of: editMode) { oldValue, newValue in
if !newValue {
// Reset drag state when leaving edit mode
draggedID = nil
dragOffsetX = 0
lastTranslationX = 0
}
}
}
// MARK: - Drag logic (smooth, order-based)
private func handleDragChanged(for item: CanvasItem, value: DragGesture.Value) {
guard editMode else { return }
// Start of drag
if draggedID == nil {
draggedID = item.id
dragOffsetX = 0
lastTranslationX = value.translation.width
return
}
// Only track the currently dragged item
guard draggedID == item.id,
let currentIndex = items.firstIndex(where: { $0.id == item.id }) else { return }
// Incremental delta instead of full translation
let deltaX = value.translation.width - lastTranslationX
lastTranslationX = value.translation.width
dragOffsetX += deltaX
// Move right
if dragOffsetX > cellWidth / 2, currentIndex < items.count - 1 {
let newIndex = currentIndex + 1
withAnimation(.spring(response: 0.25, dampingFraction: 0.9)) {
items.move(fromOffsets: IndexSet(integer: currentIndex),
toOffset: newIndex + 1)
}
// Keep visual position continuous
dragOffsetX -= cellWidth
}
// Move left
else if dragOffsetX < -cellWidth / 2, currentIndex > 0 {
let newIndex = currentIndex - 1
withAnimation(.spring(response: 0.25, dampingFraction: 0.9)) {
items.move(fromOffsets: IndexSet(integer: currentIndex),
toOffset: newIndex)
}
dragOffsetX += cellWidth
}
}
private func handleDragEnded(for item: CanvasItem, value: DragGesture.Value) {
guard draggedID == item.id else { return }
withAnimation(.spring(response: 0.3, dampingFraction: 0.9)) {
dragOffsetX = 0
}
draggedID = nil
lastTranslationX = 0
}
}
struct CanvasItemView: View {
let item: CanvasItem
let size: CGSize
let isDragging: Bool
let editMode: Bool
let horizontalOffset: CGFloat
let onDragChanged: (DragGesture.Value) -> Void
let onDragEnded: (DragGesture.Value) -> Void
var body: some View {
ZStack {
Rectangle()
.fill(item.color.opacity(isDragging ? 0.7 : 0.8))
.frame(width: size.width, height: size.height)
.overlay(
Text("Item \(item.id.uuidString.prefix(4))")
.foregroundColor(.white)
)
.scaleEffect(isDragging ? 1.05 : 1.0)
.shadow(color: .black.opacity(0.25),
radius: isDragging ? 10 : 0,
x: 0,
y: isDragging ? 6 : 0)
if editMode {
VStack {
Spacer()
Image(systemName: "line.3.horizontal")
.font(.title2)
.foregroundColor(.black)
.padding(.bottom, 8)
}
.frame(width: size.width)
}
}
.offset(x: horizontalOffset, y: 0)
.zIndex(isDragging ? 1 : 0)
.gesture(
editMode ?
DragGesture()
.onChanged(onDragChanged)
.onEnded(onDragEnded)
: nil
)
}
}

Irgendwelche Vorschläge, wie man die Daten/Gesten so strukturieren kann, dass sich das Ziehen und Neuanordnen reibungslos anfühlt?
Mobile version