SwiftUI: Неправильное движение стикера после поворота

Вопрос или проблема

Я работаю над проектом 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: Исправление проблемы с движением стикеров после их поворота

Ваша проблема связана с неправильным перемещением стикеров в SwiftUI после их поворота. Это распространенная ситуация, где при вращении объекта его координаты меток могут не соответствовать исходным осям, что вызывает путаницу при перетаскивании. Давайте разберем эту проблему более подробно и предложим оптимальное решение.

Описание проблемы

Вы упомянули, что в вашей реализации, после поворота стикера на 90 градусов, движение его вдоль «вертикальных» или «горизонтальных» осей вызывает перемещение вдоль исходных, не вращенных осей. Это происходит из-за необходимости корректировки вектора перемещения после изменения ориентации объекта.

Анализ кода

Ваш код обрабатывает перемещение и вращение с помощью кастомных жестов, что само по себе является хорошей практикой. Однако, в методе adjustTranslationForRotation и при использовании функции dragGesture, вы пытаетесь учитывать вращение с помощью тригонометрии, что может привнести ненужную сложность и ошибки в расчетах.

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

Рекомендации по исправлению

На самом деле, вам не нужно вручную применять коррекцию для перемещения. SwiftUI хорошо справляется с преобразованиями, доступными из самого системного API. Ваша функция dragGesture может быть упрощена до стандартного перемещения:

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

Таким образом, вы избегаете необходимости вручную пересчитывать положение стикера после его вращения. SwiftUI автоматически применит необходимые трансформации, и ваше приложение будет работать, как ожидается.

Дополнительные замечания

  1. Проблемы с жестами в симуляторе – как вы сами отметили, наблюдение за работой жестов может давать разные результаты в симуляторе и на реальном устройстве. Регулярно тестируйте на реальном оборудовании.

  2. Обеспечение отзывчивого интерфейса – убедитесь, что размер и положение стикеров правильно обновляются в зависимости от размера экрана и ориентации, чтобы пользователям было удобно взаимодействовать с элементами интерфейса.

  3. Тестирование на разных устройствах – после исправления кода обязательно протестируйте работу на устройствах с различными размерами экранов и версиями iOS.

Подводя итог, правильные исправления и упрощения кода помогут устранить проблему с перемещением стикеров после их вращения. SwiftUI предоставляет мощные механизмы для управления жестами и преобразованиями, которые при правильном использовании значительно упрощают разработку и обеспечивают ожидаемое поведение интерфейсов.

Оцените материал
Добавить комментарий

Капча загружается...