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

SwiftUI中签名图片定位与缩放异常问题求助

SwiftUI签名图片叠加异常问题排查与修复

核心问题原因拆解

1. 签名缩放无效果

  • 签名外层ZStack被固定了frame(width:100, height:100),即使设置scaleEffect(scaling),固定尺寸会限制显示大小,导致缩放视觉上无变化。
  • 未给签名绑定缩放手势,scaling变量始终为初始值1.0,没有更新逻辑。

2. 保存后签名偏移至左上角

  • combineImages方法直接使用视图坐标系的location点作为签名在原图的绘制位置,未考虑:
    • 主图的scale和offset导致的坐标系偏移
    • 视图尺寸与原图尺寸的比例差异
    • 签名自身的scaling缩放和rotationAngle旋转参数

3. 签名拖拽定位不准确

  • 拖拽手势simpleDrag未结合主图的缩放比例,导致拖拽位移与实际显示位置不匹配;且fingerDrag手势无实际作用,属于冗余逻辑。

修复后的完整代码

struct SignatureAddingView: View {
    
    @State var scannedImage: UIImage = UIImage()
    
    @State private var scale: CGFloat = 1
    @State private var scaleAnchor: UnitPoint = .center
    @State private var lastScale: CGFloat = 1
    @State private var offset: CGSize = .zero
    @State private var lastOffset: CGSize = .zero
        
    @State private var location: CGPoint = CGPoint(x: 100, y: 100)
    @State private var scaling: CGFloat = 1.0
    @State private var rotationAngle: Double = 0.0
        
    var body: some View {
        VStack {
            GeometryReader { geometry in
                let magnificationGesture = MagnificationGesture()
                    .onChanged{ gesture in
                        scaleAnchor = .center
                        scale = lastScale * gesture
                    }
                    .onEnded { _ in
                        fixOffsetAndScale(geometry: geometry)
                    }
                
                let dragGesture = DragGesture()
                    .onChanged { gesture in
                        var newOffset = lastOffset
                        newOffset.width += gesture.translation.width
                        newOffset.height += gesture.translation.height
                        offset = newOffset
                    }
                    .onEnded { _ in
                        fixOffsetAndScale(geometry: geometry)
                    }
                
                ZStack {
                    // 主图层
                    Image(uiImage: scannedImage)
                        .resizable()
                        .scaledToFit()
                        .position(x: geometry.size.width / 2, y: geometry.size.height / 2)
                        .scaleEffect(scale, anchor: scaleAnchor)
                        .offset(offset)
                        .gesture(dragGesture)
                        .gesture(magnificationGesture)
                    
                    // 签名层
                    if let signatureImage = loadImageFromDocumentDirectory(filename: "signature.png") {
                        ZStack {
                            Rectangle()
                                .stroke(style: StrokeStyle(lineWidth: 1, dash: [5]))
                                .fill(.blue)
                            
                            Image(uiImage: signatureImage)
                                .resizable()
                                .scaledToFit()
                        }
                        .overlay(
                            VStack {
                                Spacer()
                                HStack {
                                    Spacer()
                                    Circle()
                                        .fill(Color.green)
                                        .frame(width: 20, height: 20)
                                }
                                .padding(.trailing, -18)
                                .padding(.bottom, -13)
                            }
                        )
                        .position(location)
                        .rotationEffect(.degrees(rotationAngle), anchor: .center)
                        .scaleEffect(scaling)
                        // 绑定签名的拖拽、缩放、旋转手势
                        .gesture(
                            SimultaneousGesture(
                                DragGesture()
                                    .onChanged { value in
                                        // 结合主图缩放比例,修正拖拽位移
                                        location.x += value.translation.width / scale
                                        location.y += value.translation.height / scale
                                    },
                                MagnificationGesture()
                                    .onChanged { value in
                                        scaling *= value
                                    }
                            )
                            .simultaneously(with: RotationGesture()
                                .onChanged { angle in
                                    rotationAngle = angle.degrees
                                }
                            )
                        )
                    }
                }
                .frame(width: geometry.size.width, height: geometry.size.height)
            }
        }
        .background(Color.black.opacity(0.3))
        .overlay(
            VStack {
                Spacer()
                Button {
                    saveImageWithSignatures()
                } label: {
                    Text("Save")
                }
            }
        )
        .edgesIgnoringSafeArea(.all)
    }
    
    // 修复坐标系转换与签名绘制逻辑
    func combineImages() -> UIImage? {
        guard let signatureImage = loadImageFromDocumentDirectory(filename: "signature.png") else { return nil }
        
        // 计算视图与原图的比例关系
        let viewSize = UIScreen.main.bounds.size
        let originalScale = scannedImage.size.width / scannedImage.size.height >= viewSize.width / viewSize.height ?
            viewSize.width / scannedImage.size.width :
            viewSize.height / scannedImage.size.height
        let scaledMainImageSize = CGSize(
            width: scannedImage.size.width * originalScale * scale,
            height: scannedImage.size.height * originalScale * scale
        )
        
        // 计算主图在视图中的实际原点
        let mainImageOrigin = CGPoint(
            x: (viewSize.width - scaledMainImageSize.width)/2 + offset.width,
            y: (viewSize.height - scaledMainImageSize.height)/2 + offset.height
        )
        
        // 将签名的视图坐标转换为原图坐标
        let signatureViewRect = CGRect(
            origin: CGPoint(
                x: location.x - mainImageOrigin.x - (signatureImage.size.width * scaling)/2,
                y: location.y - mainImageOrigin.y - (signatureImage.size.height * scaling)/2
            ),
            size: CGSize(
                width: signatureImage.size.width * scaling,
                height: signatureImage.size.height * scaling
            )
        )
        let signatureOriginalRect = CGRect(
            x: signatureViewRect.origin.x / (originalScale * scale),
            y: signatureViewRect.origin.y / (originalScale * scale),
            width: signatureViewRect.size.width / (originalScale * scale),
            height: signatureViewRect.size.height / (originalScale * scale)
        )
        
        let renderer = UIGraphicsImageRenderer(size: scannedImage.size)
        return renderer.image { ctx in
            scannedImage.draw(in: CGRect(origin: .zero, size: scannedImage.size))
            
            // 应用签名旋转
            ctx.cgContext.saveGState()
            let rotationRadians = CGFloat(rotationAngle * Double.pi / 180)
            let rotationCenter = CGPoint(
                x: signatureOriginalRect.midX,
                y: signatureOriginalRect.midY
            )
            ctx.cgContext.translateBy(x: rotationCenter.x, y: rotationCenter.y)
            ctx.cgContext.rotate(by: rotationRadians)
            ctx.cgContext.translateBy(x: -rotationCenter.x, y: -rotationCenter.y)
            
            signatureImage.draw(in: signatureOriginalRect)
            ctx.cgContext.restoreGState()
        }
    }

    func saveImageWithSignatures() {
        if let combinedImage = combineImages() {
            saveImageToDocumentDirectory(image: combinedImage, filename: "modified_img.jpeg")
        }
    }
    
    func saveImageToDocumentDirectory(image: UIImage, filename: String) {
        if let data = image.jpegData(compressionQuality: 1.0) {
            let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
            let fileURL = documentsDirectory.appendingPathComponent(filename)
            do {
                try data.write(to: fileURL)
                print("Image saved to document directory: \(fileURL)")
            } catch {
                print("Error saving image: \(error)")
            }
        }
    }
    
    func loadImageFromDocumentDirectory(filename: String) -> UIImage? {
        let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        let fileURL = documentsDirectory.appendingPathComponent(filename)
        
        do {
            let imageData = try Data(contentsOf: fileURL)
            return UIImage(data: imageData)
        } catch {
            print("Error loading image: \(error)")
            return nil
        }
    }
    
    private func fixOffsetAndScale(geometry: GeometryProxy) {
        let newScale: CGFloat = .minimum(.maximum(scale, 1), 4)
        let screenSize = geometry.size
        
        let originalScale = scannedImage.size.width / scannedImage.size.height >= screenSize.width / screenSize.height ?
            geometry.size.width / scannedImage.size.width :
            geometry.size.height / scannedImage.size.height
        
        let imageWidth = (scannedImage.size.width * originalScale) * newScale
        
        var width: CGFloat = .zero
        if imageWidth > screenSize.width {
            let widthLimit: CGFloat = imageWidth > screenSize.width ?
                (imageWidth - screenSize.width) / 2
                : 0

            width = offset.width > 0 ?
                .minimum(widthLimit, offset.width) :
                .maximum(-widthLimit, offset.width)
        }
        
        let imageHeight = (scannedImage.size.height * originalScale) * newScale
        var height: CGFloat = .zero
        if imageHeight > screenSize.height {
            let heightLimit: CGFloat = imageHeight > screenSize.height ?
                (imageHeight - screenSize.height) / 2
                : 0

            height = offset.height > 0 ?
                .minimum(heightLimit, offset.height) :
                .maximum(-heightLimit, offset.height)
        }
        
        let newOffset = CGSize(width: width, height: height)
        lastScale = newScale
        lastOffset = newOffset
        withAnimation() {
            offset = newOffset
            scale = newScale
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 23:27:07