iOS SwiftUI缩放图片后签名截图裁剪问题及修改需求
iOS图片签名保存功能快照裁剪问题修复方案
问题背景
开发iOS应用时,实现了图片添加签名并本地保存的功能,但在图片缩放后调用captureSnapshot函数时,出现图片被裁剪的问题,不符合预期。
需求目标
- 支持图片缩放操作
- 截取包含签名的完整缩放区域快照
- 确保签名在快照中正常显示
- 缩放回原比例后,签名可见且图片状态保持不变
现有代码片段
状态变量定义
// 图片相关状态 @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 debug = "" // 签名相关状态 @State private var location: CGPoint = CGPoint(x: 150, y: 300) @GestureState private var fingerLocation: CGPoint? = nil @GestureState private var startLocation: CGPoint? = nil
快照捕获函数及扩展
// 快照捕获主函数 private func captureSnapshot(rect: CGRect) -> UIImage? { var result: UIImage? if let scene = UIApplication.shared.connectedScenes.first( where: { $0.activationState == .foregroundActive } ) as? UIWindowScene { result = scene.windows[0].rootViewController?.view.asImage(rect: rect) } return result } extension UIView { func asImage(rect: CGRect) -> UIImage { let renderer = UIGraphicsImageRenderer(bounds: rect) return renderer.image { rendererContext in layer.render(in: rendererContext.cgContext) } } }
图片视图及交互代码
// 图片视图主体 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) } // 图片展示 Image(uiImage: scannedImage) .resizable() .border(.red, width: 2) .position(x: geometry.size.width / 2, y: geometry.size.height / 2) .scaleEffect(scale, anchor: scaleAnchor) .offset(offset) .gesture(dragGesture) .gesture(magnificationGesture) .overlay ( // 签名Overlay VStack { ZStack { ZStack(alignment: .bottomTrailing) { Rectangle() .stroke(style: StrokeStyle(lineWidth: 2, dash: [5])) .fill(.blue) .opacity(opacity) Rectangle() .fill(.clear) Image(uiImage: signImage) .resizable() .scaledToFit() // 保存按钮 VStack { HStack { Spacer() Circle() .fill(Color.green) .frame(width: 25, height: 25) .onTapGesture { opacity = 0.0 DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: { // 保存快照 scannedImage = captureSnapshot(rect: geometry.frame(in: .global))! opacity = 1.0 }) } } Spacer() } .opacity(opacity) .zIndex(2) } .frame(width: width, height: height) } } .frame(maxWidth: width, maxHeight: height, alignment: .center) .position(location) .gesture( simpleDrag.simultaneously(with: fingerDrag) ) ) .frame(width: geometry.size.width, height: geometry.size.height) } Spacer() }
缩放偏移修正函数
// 图片缩放与偏移修正 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 } }
问题原因及修复方案
问题根源
当前captureSnapshot函数截取的是GeometryProxy的全局frame(即屏幕可视区域范围),但图片缩放后超出屏幕的部分会被视图裁剪,导致快照只包含屏幕内的部分,而非完整的缩放后图片加签名。
修复步骤
- 调整快照捕获逻辑,不再依赖屏幕可视区域,而是基于图片实际缩放尺寸渲染完整内容
- 使用SwiftUI专用的视图快照方法,确保正确捕获缩放后的视图层级
- 修正保存后的视图状态,确保缩放回原比例时签名可见
修改后的代码
替换快照捕获相关代码
// 新的快照捕获函数,捕获完整缩放后的图片+签名 private func captureFullSnapshot() -> UIImage? { // 按图片实际缩放尺寸创建临时视图,包含图片和签名 let tempView = Image(uiImage: scannedImage) .resizable() .scaleEffect(scale, anchor: scaleAnchor) .offset(offset) .overlay( Image(uiImage: signImage) .resizable() .scaledToFit() .frame(width: width, height: height) .position(location) ) .frame(width: scannedImage.size.width * scale, height: scannedImage.size.height * scale) return tempView.snapshot() } // SwiftUI View扩展:实现完整视图快照 extension View { func snapshot() -> UIImage? { let controller = UIHostingController(rootView: self) let view = controller.view let targetSize = controller.view.intrinsicContentSize view?.bounds = CGRect(origin: .zero, size: targetSize) view?.backgroundColor = .clear let renderer = UIGraphicsImageRenderer(size: targetSize) return renderer.image { _ in view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true) } } }
修改保存按钮的调用逻辑
在绿色圆形按钮的onTapGesture中替换快照调用:
.onTapGesture { opacity = 0.0 DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: { if let fullSnapshot = captureFullSnapshot() { scannedImage = fullSnapshot } opacity = 1.0 // 缩放回原比例,重置偏移,确保签名可见 withAnimation { scale = 1 offset = .zero lastScale = 1 lastOffset = .zero } }) }
额外说明
- 新的
captureFullSnapshot函数创建临时视图,按图片实际缩放尺寸渲染,避免屏幕可视区域的裁剪限制 View.snapshot扩展通过UIHostingController完整渲染SwiftUI视图层级,生成无裁剪的快照- 保存后自动重置缩放和偏移,确保签名在原比例下可见,图片状态恢复初始缩放状态
内容的提问来源于stack exchange,提问作者Rin Star
相关产品推荐
相关产品推荐

