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
相关产品推荐
相关产品推荐

