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

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

Сообщение Anonymous »

Я создаю горизонтальное представление в стиле «карусели» в SwiftUI, где элементы можно менять путем перетаскивания (аналогично временной шкале редактора).
Основная идея:
Элементы располагаются в горизонтальной дорожке внутри 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
)
}
}

like
Изображение

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

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

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

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

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

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

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