You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI图片裁剪器旋转与裁剪功能异常问题求助

SwiftUI图片裁剪器问题修复

原代码存在的问题

  • 旋转图片后,裁剪会包含视图背景(图片高度缩短导致裁剪区域越界)
  • 裁剪无法匹配裁剪框的实际位置
  • 裁剪框无法调整大小

修复后的完整代码

import SwiftUI

struct ContentView: View {
    @State private var rotationAngle: Double = 0
    @State private var cropRect = CGRect(x: 0, y: 0, width: 200, height: 200)
    @State var originalImage: UIImage
    @State var displayedImage: UIImage
    
    // 计算图片在视图中的缩放比例和偏移量
    private func imageScaleAndOffset(in geo: GeometryProxy) -> (scale: CGFloat, offset: CGSize) {
        let imageRatio = originalImage.size.width / originalImage.size.height
        let viewRatio = geo.size.width / geo.size.height
        
        let scale: CGFloat
        let offset: CGSize
        
        if imageRatio > viewRatio {
            scale = geo.size.width / originalImage.size.width
            let scaledHeight = originalImage.size.height * scale
            offset = CGSize(width: 0, height: (geo.size.height - scaledHeight) / 2)
        } else {
            scale = geo.size.height / originalImage.size.height
            let scaledWidth = originalImage.size.width * scale
            offset = CGSize(width: (geo.size.width - scaledWidth) / 2, height: 0)
        }
        
        return (scale, offset)
    }
    
    // 将视图坐标转换为原始图片的坐标(处理旋转和缩放)
    private func convertViewRectToImageRect(viewRect: CGRect, in geo: GeometryProxy) -> CGRect {
        let (scale, offset) = imageScaleAndOffset(in: geo)
        let rotatedRect = viewRect.applying(CGAffineTransform(rotationAngle: -rotationAngle * .pi / 180))
        
        let x = (rotatedRect.origin.x - offset.width) / scale
        let y = (rotatedRect.origin.y - offset.height) / scale
        let width = rotatedRect.size.width / scale
        let height = rotatedRect.size.height / scale
        
        return CGRect(x: x, y: y, width: width, height: height)
    }
    
    var body: some View {
        VStack(spacing: 20) {
            Button(action: {
                withAnimation {
                    rotationAngle = (rotationAngle + 90).truncatingRemainder(dividingBy: 360)
                }
            }) {
                Text("旋转")
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
            
            Button(action: {
                guard let cgImage = originalImage.cgImage else { return }
                let geoProxy = GeometryReader { $0 }.frame(height: 500).sizeThatFits(.infinity)
                let imageCropRect = convertViewRectToImageRect(viewRect: cropRect, in: GeometryProxy(size: geoProxy))
                
                // 确保裁剪区域在图片范围内
                let normalizedRect = CGRect(
                    x: max(imageCropRect.origin.x, 0),
                    y: max(imageCropRect.origin.y, 0),
                    width: min(imageCropRect.width, cgImage.width - imageCropRect.origin.x),
                    height: min(imageCropRect.height, cgImage.height - imageCropRect.origin.y)
                )
                
                if let croppedCGImage = cgImage.cropping(to: normalizedRect) {
                    let croppedImage = UIImage(cgImage: croppedCGImage)
                    originalImage = croppedImage
                    displayedImage = croppedImage
                    // 重置裁剪框为新图片的视图尺寸
                    DispatchQueue.main.async {
                        cropRect = CGRect(x: 0, y: 0, width: geoProxy.width, height: geoProxy.height)
                    }
                }
            }) {
                Text("裁剪")
                    .padding()
                    .background(Color.green)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
            
            ZStack {
                GeometryReader { geo in
                    Image(uiImage: displayedImage)
                        .resizable()
                        .scaledToFit()
                        .rotationEffect(.degrees(rotationAngle))
                        .frame(width: geo.size.width, height: geo.size.height)
                    
                    // 裁剪框
                    Rectangle()
                        .stroke(Color.white, lineWidth: 2)
                        .background(Color.black.opacity(0.5))
                        .position(x: cropRect.midX, y: cropRect.midY)
                        .frame(width: cropRect.size.width, height: cropRect.size.height)
                        .rotationEffect(.degrees(rotationAngle))
                        // 拖拽手势
                        .gesture(
                            DragGesture()
                                .onChanged { value in
                                    let x = value.location.x - cropRect.width / 2
                                    let y = value.location.y - cropRect.height / 2
                                    let maxX = geo.size.width - cropRect.width
                                    let maxY = geo.size.height - cropRect.height
                                    
                                    cropRect.origin.x = min(max(x, 0), maxX)
                                    cropRect.origin.y = min(max(y, 0), maxY)
                                }
                        )
                        // 缩放手势
                        .gesture(
                            MagnificationGesture()
                                .onChanged { value in
                                    let (scale, _) = imageScaleAndOffset(in: geo)
                                    let minSize = min(originalImage.size.width, originalImage.size.height) * scale * 0.2
                                    let maxSize = min(geo.size.width, geo.size.height)
                                    
                                    let newWidth = cropRect.width * value
                                    let newHeight = cropRect.height * value
                                    
                                    cropRect.size.width = min(max(newWidth, minSize), maxSize)
                                    cropRect.size.height = min(max(newHeight, minSize), maxSize)
                                    
                                    // 保持裁剪框居中缩放
                                    cropRect.origin.x = cropRect.midX - cropRect.width / 2
                                    cropRect.origin.y = cropRect.midY - cropRect.height / 2
                                }
                        )
                        .onAppear {
                            cropRect = CGRect(x: 0, y: 0, width: geo.size.width, height: geo.size.height)
                        }
                }
            }
            .frame(height: 500)
            .background(Color.gray.opacity(0.2))
        }
        .padding()
        .onAppear {
            displayedImage = originalImage
        }
    }
}

问题修复说明

1. 旋转后裁剪包含视图背景

  • 核心原因:旋转后图片的显示区域与原始图片坐标未对应,直接用视图坐标裁剪会包含背景
  • 修复方式:
    • 新增imageScaleAndOffset方法,计算图片在视图中的缩放比例和偏移量,确保图片始终按比例适配视图
    • 新增convertViewRectToImageRect方法,将视图裁剪框坐标转换为原始图片坐标,同时处理旋转角度的逆变换,保证裁剪区域匹配原始图片像素

2. 裁剪无法匹配裁剪框实际位置

  • 核心原因:原代码未考虑图片缩放、偏移和旋转带来的坐标变换,直接用视图坐标裁剪原始图片
  • 修复方式:
    • 裁剪前通过convertViewRectToImageRect转换坐标
    • 添加边界检查,确保裁剪范围不超出原始图片像素范围,避免越界崩溃

3. 裁剪框无法调整大小

  • 核心原因:原代码仅实现拖拽手势,缺少缩放手势
  • 修复方式:
    • 给裁剪框添加MagnificationGesture缩放手势,支持双指调整大小
    • 设置裁剪框最小/最大尺寸限制,避免过小或超出视图范围
    • 缩放时保持裁剪框居中,优化交互体验

内容的提问来源于stack exchange,提问作者alwer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 21:37:38