Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-09 10:16:00 -06:00
parent 1258f5dcd3
commit 05aedadfc3

View File

@ -46,6 +46,9 @@ struct PhotoCropperSheet: View {
@State private var lastScale: CGFloat = 1.0
@State private var offset: CGSize = .zero
@State private var lastOffset: CGSize = .zero
@State private var rotation: Angle = .zero
@State private var lastRotation: Angle = .zero
@State private var fixedRotation: Angle = .zero // For 90° toolbar rotations
@State private var containerSize: CGSize = .zero
// Horizontal padding for the crop area
@ -84,10 +87,11 @@ struct PhotoCropperSheet: View {
Image(uiImage: uiImage)
.resizable()
.scaledToFill()
.rotationEffect(rotation + fixedRotation)
.scaleEffect(scale)
.offset(offset)
.gesture(dragGesture)
.gesture(magnificationGesture)
.gesture(combinedPinchRotateGesture)
.frame(width: geometry.size.width, height: geometry.size.height)
.clipped()
}
@ -119,11 +123,30 @@ struct PhotoCropperSheet: View {
.foregroundStyle(.white)
}
ToolbarItem(placement: .principal) {
Button {
resetTransform()
} label: {
Image(systemName: "arrow.counterclockwise")
.foregroundStyle(.white)
HStack(spacing: Design.Spacing.xLarge) {
// Rotate 90° left
Button {
rotate90Left()
} label: {
Image(systemName: "rotate.left")
.foregroundStyle(.white)
}
// Reset all transforms
Button {
resetTransform()
} label: {
Image(systemName: "arrow.counterclockwise")
.foregroundStyle(.white)
}
// Rotate 90° right
Button {
rotate90Right()
} label: {
Image(systemName: "rotate.right")
.foregroundStyle(.white)
}
}
}
ToolbarItem(placement: .confirmationAction) {
@ -153,16 +176,28 @@ struct PhotoCropperSheet: View {
}
}
private var magnificationGesture: some Gesture {
MagnificationGesture()
.onChanged { value in
let newScale = lastScale * value
private var combinedPinchRotateGesture: some Gesture {
SimultaneousGesture(
MagnificationGesture(),
RotationGesture()
)
.onChanged { value in
// Handle pinch (magnification)
if let magnification = value.first {
let newScale = lastScale * magnification
// Limit scale between 0.5x and 5x
scale = min(max(newScale, 0.5), 5.0)
}
.onEnded { _ in
lastScale = scale
// Handle rotation
if let rotationValue = value.second {
rotation = lastRotation + rotationValue
}
}
.onEnded { _ in
lastScale = scale
lastRotation = rotation
}
}
// MARK: - Actions
@ -173,6 +208,21 @@ struct PhotoCropperSheet: View {
lastScale = 1.0
offset = .zero
lastOffset = .zero
rotation = .zero
lastRotation = .zero
fixedRotation = .zero
}
}
private func rotate90Left() {
withAnimation(.spring(duration: Design.Animation.springDuration)) {
fixedRotation -= .degrees(90)
}
}
private func rotate90Right() {
withAnimation(.spring(duration: Design.Animation.springDuration)) {
fixedRotation += .degrees(90)
}
}
@ -186,7 +236,11 @@ struct PhotoCropperSheet: View {
}
// First, normalize the image orientation so we're working with correctly oriented pixels
let normalizedImage = normalizeImageOrientation(uiImage)
let orientedImage = normalizeImageOrientation(uiImage)
// Apply total rotation (gesture rotation + fixed rotation)
let totalRotation = rotation + fixedRotation
let normalizedImage = rotateImage(orientedImage, by: totalRotation)
let imageSize = normalizedImage.size
// Guard against zero container size
@ -321,6 +375,44 @@ struct PhotoCropperSheet: View {
image.draw(at: .zero)
}
}
/// Rotates an image by the specified angle
private func rotateImage(_ image: UIImage, by angle: Angle) -> UIImage {
// If no rotation needed, return original
guard abs(angle.radians) > 0.001 else { return image }
let radians = CGFloat(angle.radians)
// Calculate the size of the rotated image
let rotatedRect = CGRect(origin: .zero, size: image.size)
.applying(CGAffineTransform(rotationAngle: radians))
let rotatedSize = CGSize(
width: abs(rotatedRect.width),
height: abs(rotatedRect.height)
)
let format = UIGraphicsImageRendererFormat()
format.scale = 1 // Use 1:1 scale for output
let renderer = UIGraphicsImageRenderer(size: rotatedSize, format: format)
return renderer.image { context in
let cgContext = context.cgContext
// Move to center
cgContext.translateBy(x: rotatedSize.width / 2, y: rotatedSize.height / 2)
// Rotate
cgContext.rotate(by: radians)
// Draw image centered
image.draw(in: CGRect(
x: -image.size.width / 2,
y: -image.size.height / 2,
width: image.size.width,
height: image.size.height
))
}
}
}
// MARK: - Crop Overlay