iosswiftxcodeswiftui

SwiftUI: Incorrect Sticky Note Movement After Rotation


I’m working on a SwiftUI project where I can drag, resize, and rotate sticky notes. While the resizing and rotation work fine, I’m facing issues with the dragging behavior after rotating a sticky note.

Problem:

When the sticky note is rotated, dragging it along what should be the “vertical” or “horizontal” axes results in movement along the original unrotated axes. For example, if the sticky note is rotated 90 degrees, dragging it upwards causes it to move sideways.

What I’ve tried:

Here is my Code for the Sticky Note:

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

And here the code for movement and rotation:

static func adjustTranslationForRotation(_ translation: CGSize, rotation: Angle) -> CGSize {
    let radians = rotation.radians
    let cosTheta = CGFloat(cos(radians))
    let sinTheta = CGFloat(sin(radians))

    // Adjust the translation to account for rotation
    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
            }
        }
}

Has anyone experienced a similar issue where the dragging direction is incorrect after applying a rotation? Any advice on how to make the sticky note move correctly based on its current orientation would be greatly appreciated!

Thanks in advance!

Edit: Here is a minimal reproductive example

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 {
    // Adjust the translation to account for the center of the StickyNote
    static func adjustTranslationForRotation(_ translation: CGSize, rotation: Angle) -> CGSize {
        let radians = rotation.radians
        let cosTheta = CGFloat(cos(radians))
        let sinTheta = CGFloat(sin(radians))

        // Transformation der Translation in das rotierte Koordinatensystem
        let adjustedX = translation.width * cosTheta - translation.height * sinTheta
        let adjustedY = translation.width * sinTheta + translation.height * cosTheta

        return CGSize(width: adjustedX, height: adjustedY)
    }

    // Drag Gesture Handler
    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
                }
            }
    }

    // Rotation Gesture Handler
    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
            }
    }
}

Solution

  • There is no need to adjust the translation for the rotation, just let the modifiers do the work for you.

    So the static function adjustTranslationForRotation is not needed and the function dragGesture can be changed as follows:

    // static func 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
    }
    

    Animation

    What I also noticed is that the gestures (especially the rotation) only seem to work properly when the square is near the center of the display. However, this might just be an issue with using a simulator.