Основная идея:
Элементы располагаются в горизонтальной дорожке внутри ScrollView(.horizontal).
В режиме редактирования вы можете перетаскивать элемент горизонтально.
Во время перетаскивания я обновляю массив и пересчитать позиции всех элементов, чтобы изменить их порядок в реальном времени.
Приведенный ниже код представляет собой минимальный пример. Функционально это работает, но перетаскивание не кажется плавным:
перетаскиваемый элемент дрожит, а другие элементы подпрыгивают во время перетаскивания.
Мой вопрос:
Какие изменения мне следует внести в эту реализацию, чтобы взаимодействие перетаскивания с изменением порядка было плавным и непрерывным (без дрожания) при перетаскивании элементов по горизонтали?
Вот демонстрационный код:
Код: Выделить всё
import SwiftUI
struct CanvasItem: Identifiable, Equatable {
let id = UUID()
var position: CGPoint
var size: CGSize = CGSize(width: 100, height: 100)
var color: Color = .blue
}
struct HorizontalCanvasView: View {
@State private var items: [CanvasItem] = [
CanvasItem(position: CGPoint(x: 200, y: 150), color: .blue),
CanvasItem(position: CGPoint(x: 320, y: 150), color: .red),
CanvasItem(position: CGPoint(x: 440, y: 150), color: .green)
]
@State private var displayItems: [CanvasItem] = []
@State private var draggedID: UUID? = nil
@State private var dragTranslation: CGSize = .zero
@State private var editMode: Bool = false
private let canvasHeight: CGFloat = 300
private let itemSpacing: CGFloat = 120
private let startX: CGFloat = 200
private let reorderThreshold: CGFloat = 60
private let padding: CGFloat = 200
private var totalWidth: CGFloat {
let numItems = CGFloat(items.count)
return max(800, startX + (numItems * itemSpacing) + padding)
}
init() {
_displayItems = State(initialValue: [])
}
private func updatePositions(for array: inout [CanvasItem]) {
for (index, _) in array.enumerated() {
array[index].position.x = startX + CGFloat(index) * itemSpacing
array[index].position.y = canvasHeight / 2
}
}
private func insertionIndex(for x: CGFloat, excluding draggedID: UUID) -> Int {
let others = displayItems
.filter { $0.id != draggedID }
.sorted { $0.position.x < $1.position.x }
var index = 0
for other in others {
if x < other.position.x + reorderThreshold {
return index
}
index += 1
}
return index
}
private func updateLiveReorder(for draggedX: CGFloat) {
guard
let id = draggedID,
let draggedIndex = displayItems.firstIndex(where: { $0.id == id })
else { return }
var temp = displayItems
let dragged = temp.remove(at: draggedIndex)
let newIndex = insertionIndex(for: draggedX, excluding: id)
temp.insert(dragged, at: newIndex)
updatePositions(for: &temp)
if let finalDraggedIndex = temp.firstIndex(where: { $0.id == id }) {
temp[finalDraggedIndex].position.x = draggedX
}
displayItems = temp
}
private func commitReorder() {
items = displayItems
draggedID = nil
dragTranslation = .zero
updatePositions(for: &items)
updatePositions(for: &displayItems)
}
private func toggleEditMode() {
if !editMode {
editMode = true
} else {
editMode = false
draggedID = nil
dragTranslation = .zero
updatePositions(for: &displayItems)
}
}
var body: some View {
VStack {
Button(action: toggleEditMode) {
Text(editMode ? "Done" : "Edit")
.font(.headline)
.padding()
.background(editMode ? Color.red : Color.blue)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
.padding()
GeometryReader { _ in
ScrollView(.horizontal, showsIndicators: false) {
ZStack(alignment: .topLeading) {
Rectangle()
.fill(Color.gray.opacity(0.1))
.frame(width: totalWidth, height: canvasHeight)
ForEach(displayItems) { item in
CanvasItemView(
item: item,
draggedID: $draggedID,
dragTranslation: $dragTranslation,
canvasWidth: totalWidth,
isDragging: draggedID == item.id,
editMode: editMode,
onDrag: { newX in
updateLiveReorder(for: newX)
},
onEnd: {
commitReorder()
}
)
}
}
.frame(width: totalWidth, height: canvasHeight)
.animation(.spring(response: 0.5, dampingFraction: 0.9), value: displayItems)
}
.frame(height: canvasHeight)
}
}
.onAppear {
displayItems = items
updatePositions(for: &displayItems)
}
.onChange(of: items) { newItems in
displayItems = newItems
updatePositions(for: &displayItems)
}
}
}
struct CanvasItemView: View {
let item: CanvasItem
@Binding var draggedID: UUID?
@Binding var dragTranslation: CGSize
let canvasWidth: CGFloat
let isDragging: Bool
let editMode: Bool
let onDrag: (CGFloat) -> Void
let onEnd: () -> Void
@State private var animatedOffset: CGSize = .zero
private let lerpFactor: Double = 0.15
private let minTranslationChange: CGFloat = 2.0
var body: some View {
ZStack {
Rectangle()
.fill(item.color.opacity(isDragging ? 0.7 : 0.8))
.frame(width: item.size.width, height: item.size.height)
.overlay(
Text("Item \(item.id.uuidString.prefix(4))")
.foregroundColor(.white)
)
.position(item.position)
.offset(isDragging ? animatedOffset : .zero)
.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: 30)
.allowsHitTesting(false)
}
}
.gesture(
editMode ?
DragGesture()
.onChanged { value in
if draggedID == nil {
draggedID = item.id
}
let newTranslation = value.translation
let deltaWidth = abs(newTranslation.width - dragTranslation.width)
if deltaWidth > minTranslationChange {
dragTranslation = newTranslation
let currentX = item.position.x + newTranslation.width
let clampedX = max(item.size.width / 2,
min(canvasWidth - item.size.width / 2, currentX))
animatedOffset.width += (newTranslation.width - animatedOffset.width) * lerpFactor
animatedOffset.height += (newTranslation.height - animatedOffset.height) * lerpFactor
withAnimation(.easeInOut(duration: 0.05)) {
onDrag(clampedX)
}
}
}
.onEnded { value in
let finalX = item.position.x + dragTranslation.width
let clampedFinalX = max(item.size.width / 2,
min(canvasWidth - item.size.width / 2, finalX))
withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
animatedOffset = .zero
onDrag(clampedFinalX)
onEnd()
}
}
: nil
)
}
}

Есть предложения о том, как структурировать данные/жесты, чтобы перетаскивание и изменение порядка было плавным?
Подробнее здесь: https://stackoverflow.com/questions/798 ... e-the-drag
Мобильная версия