Вопрос или проблема
Я работаю над проектом SwiftUI, где я могу перетаскивать, изменять размер и вращать стикеры. Хотя изменение размера и вращение работает хорошо, у меня возникают проблемы с поведением перетаскивания после вращения стикера.
Проблема:
Когда стикер вращается, перетаскивание его вдоль того, что должно быть «вертикальной» или «горизонтальной» осью, приводит к движению вдоль оригинальных не вращенных осей. Например, если стикер вращается на 90 градусов, его перетаскивание вверх приводит к боковому движению.
Что я пробовал:
- Я использовал rotationEffect для обработки вращения.
- Я корректирую трансляцию, учитывая вращение с помощью тригонометрии, но перетаскивание все еще не ведет себя правильно.
Вот мой код для стикера:
ZStack {
stickyNote.color
.frame(width: stickyNote.size.width, height: stickyNote.size.height)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(isSelected ? Color.blue : Color.clear, lineWidth: 3)
)
.rotationEffect(rotation, anchor: .center)
.offset(x: position.width, y: position.height)
.gesture(
TransformHelper.dragGesture(
position: $position,
lastPosition: $lastPosition,
rotation: rotation,
isResizing: isResizing,
isDragging: $isDragging
)
)
.gesture(
TransformHelper.rotationGesture(
rotation: $rotation,
lastRotation: $lastRotation
)
)
}
А вот код для перемещения и вращения:
static func adjustTranslationForRotation(_ translation: CGSize, rotation: Angle) -> CGSize {
let radians = rotation.radians
let cosTheta = CGFloat(cos(radians))
let sinTheta = CGFloat(sin(radians))
// Корректировка трансляции с учетом вращения
let adjustedX = translation.width * cosTheta - translation.height * sinTheta
let adjustedY = translation.width * sinTheta + translation.height * cosTheta
return CGSize(width: adjustedX, height: adjustedY)
}
static func dragGesture(
position: Binding<CGSize>,
lastPosition: Binding<CGSize>,
rotation: Angle,
isResizing: Bool,
isDragging: Binding<Bool>
) -> some Gesture {
return DragGesture()
.onChanged { value in
if !isResizing {
let adjustedTranslation = adjustTranslationForRotation(value.translation, rotation: rotation)
position.wrappedValue = CGSize(
width: lastPosition.wrappedValue.width + adjustedTranslation.width,
height: lastPosition.wrappedValue.height + adjustedTranslation.height
)
isDragging.wrappedValue = true
}
}
.onEnded { _ in
if isDragging.wrappedValue {
isDragging.wrappedValue = false
lastPosition.wrappedValue = position.wrappedValue
}
}
}
Кто-то сталкивался с аналогичной проблемой, когда направление перетаскивания неправильно после применения вращения? Буду признателен за любые советы о том, как правильно двигать стикер в зависимости от его текущей ориентации!
Заранее спасибо!
Правка: Вот минимальный воспроизводимый пример
import SwiftUI
struct StickyNoteView: View {
@State private var position: CGSize = .zero
@State private var lastPosition: CGSize = .zero
@State private var rotation: Angle = .zero
@State private var lastRotation: Angle = .zero
@State private var isDragging: Bool = false
var body: some View {
ZStack {
Rectangle()
.fill(Color.yellow)
.frame(width: 200, height: 200)
.rotationEffect(rotation)
.offset(x: position.width, y: position.height)
.gesture(
TransformHelper.dragGesture(
position: $position,
lastPosition: $lastPosition,
rotation: rotation,
isResizing: false,
isDragging: $isDragging
)
)
.gesture(
TransformHelper.rotationGesture(
rotation: $rotation,
lastRotation: $lastRotation
)
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray.opacity(0.3))
.edgesIgnoringSafeArea(.all)
}
}
import SwiftUI
struct TransformHelper {
// Корректировка трансляции с учетом центра стикера
static func adjustTranslationForRotation(_ translation: CGSize, rotation: Angle) -> CGSize {
let radians = rotation.radians
let cosTheta = CGFloat(cos(radians))
let sinTheta = CGFloat(sin(radians))
// Преобразование трансляции в вращенную систему координат
let adjustedX = translation.width * cosTheta - translation.height * sinTheta
let adjustedY = translation.width * sinTheta + translation.height * cosTheta
return CGSize(width: adjustedX, height: adjustedY)
}
// Обработчик жеста перетаскивания
static func dragGesture(
position: Binding<CGSize>,
lastPosition: Binding<CGSize>,
rotation: Angle,
isResizing: Bool,
isDragging: Binding<Bool>
) -> some Gesture {
return DragGesture()
.onChanged { value in
if !isResizing {
let adjustedTranslation = adjustTranslationForRotation(value.translation, rotation: rotation)
position.wrappedValue = CGSize(
width: lastPosition.wrappedValue.width + adjustedTranslation.width,
height: lastPosition.wrappedValue.height + adjustedTranslation.height
)
isDragging.wrappedValue = true
}
}
.onEnded { _ in
if isDragging.wrappedValue {
isDragging.wrappedValue = false
lastPosition.wrappedValue = position.wrappedValue
}
}
}
// Обработчик жеста вращения
static func rotationGesture(
rotation: Binding<Angle>,
lastRotation: Binding<Angle>
) -> some Gesture {
return RotationGesture()
.onChanged { angle in
rotation.wrappedValue = lastRotation.wrappedValue + angle
}
.onEnded { angle in
lastRotation.wrappedValue += angle
}
}
}
Нет необходимости корректировать трансляцию для вращения, просто позвольте модификаторам выполнять работу за вас.
Таким образом, статическая функция adjustTranslationForRotation
не нужна, а функция dragGesture
может быть изменена следующим образом:
// статическая функция dragGesture
if !isResizing {
// let adjustedTranslation = adjustTranslationForRotation(value.translation, rotation: rotation)
position.wrappedValue = CGSize(
width: lastPosition.wrappedValue.width + value.translation.width,
height: lastPosition.wrappedValue.height + value.translation.height
)
isDragging.wrappedValue = true
}
Что я также заметил, так это то, что жесты (особенно вращение) кажутся корректными только когда квадрат находится близко к центру экрана. Однако, возможно, это просто проблема с использованием симулятора.
Ответ или решение
В разработке приложений на SwiftUI часто возникают ситуации, когда необходимо использовать различные размеры и ориентации элементов интерфейса, такие как стикеры (sticky notes). Одной из распространенных проблем является некорректное поведение при перетаскивании стикера после его поворота. В этой статье мы подробно разберем проблему и предложим решение, чтобы исправить поведение перетаскивания.
Проблема
После поворота стикера означает, что его ориентация меняется. Однако, если реализовать обычный жест перетаскивания, движение будет соответствовать исходной ориентации стикера, что приведет к смещению неправильно. Это происходит, когда вы пытаетесь перетаскивать стикер по вертикали или горизонтали, но вместо этого стикер движется по диагонали.
Решение
Следует рассмотреть, как правильно обрабатывать перевод (translation) при перетаскивании стикера. Вместо того чтобы вручную корректировать значения переводов с помощью тригонометрических функций, можно просто использовать значения без каких-либо дополнительных преобразований.
Оптимизированный код жеста перетаскивания
Вместо метода adjustTranslationForRotation
, который не требуется, можно упростить код следующим образом:
static func dragGesture(
position: Binding<CGSize>,
lastPosition: Binding<CGSize>,
isResizing: Bool,
isDragging: Binding<Bool>
) -> some Gesture {
return DragGesture()
.onChanged { value in
if !isResizing {
position.wrappedValue = CGSize(
width: lastPosition.wrappedValue.width + value.translation.width,
height: lastPosition.wrappedValue.height + value.translation.height
)
isDragging.wrappedValue = true
}
}
.onEnded { _ in
if isDragging.wrappedValue {
isDragging.wrappedValue = false
lastPosition.wrappedValue = position.wrappedValue
}
}
}
В этом измененном коде мы больше не используем преобразование для поправки ориентации стикера, а просто обновляем его позицию напрямую с учетом значений value.translation
.
Общие рекомендации
-
Позиционирование: Обратите внимание, что при изменении позиции стикера также важно правильно обновлять положение при изменении ориентации или размера.
-
Тестирование на устройствах: Убедитесь, что поведение стикеров корректно тестируется не только в симуляторе, но и на реальном устройстве. Иногда поведение может отличаться, и это позволит выявить возможные проблемы с взаимодействием.
-
Использование комбинаций жестов: Если ваш интерфейс использует множественные жесты (например, поворот и перетаскивание), тщательно продумайте логику их обработки. Это поможет избежать конфликта между жестами и улучшит пользовательский опыт.
Заключение
Создание интерактивных элементов на SwiftUI хорошо подходит для задач, связанных с пользовательскими интерфейсами. Корректная реализация жестов управления восприятием и использованием вращения и перетаскивания — это ключ к успешному пользовательскому опыту. С учетом вышеописанных рекомендаций вы сможете эффективно управлять поведением стикеров в вашем приложении.