Горизонтальный вид SwiftUI с перетаскиванием для изменения порядка кажется нервным – как сделать перетаскивание плавным?IOS

Программируем под IOS
Ответить
Anonymous
 Горизонтальный вид SwiftUI с перетаскиванием для изменения порядка кажется нервным – как сделать перетаскивание плавным?

Сообщение Anonymous »

Я создаю горизонтальное представление в стиле «карусели» в SwiftUI, где элементы можно менять путем перетаскивания (аналогично временной шкале редактора).
Основная идея:
Элементы располагаются в горизонтальной дорожке внутри ScrollView(.horizontal).
В режиме редактирования вы можете перетаскивать элемент горизонтально.
Во время перетаскивания я обновляю массив и пересчитать позиции всех элементов, чтобы изменить их порядок в реальном времени.
Приведенный ниже код представляет собой минимальный пример. Функционально это работает, но перетаскивание не кажется плавным:
перетаскиваемый элемент дрожит, а другие элементы подпрыгивают, пока я перетаскиваю.
Мой вопрос:
Какие изменения мне следует внести в эту реализацию, чтобы взаимодействие перетаскивания с изменением порядка было плавным и непрерывным (без дрожания) при перетаскивании элементов по горизонтали?
автоматическая прокрутка при перетаскивании ячеек вблизи краев при горизонтальной прокрутке Canvas
Вот демо-код:

Код: Выделить всё

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
)
}
}

Есть какие-нибудь предложения о том, как структурировать данные/жесты, чтобы перетаскивание и изменение порядка было плавным?

Подробнее здесь: https://stackoverflow.com/questions/798 ... e-the-drag
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «IOS»