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