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

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(即屏幕可视区域范围),但图片缩放后超出屏幕的部分会被视图裁剪,导致快照只包含屏幕内的部分,而非完整的缩放后图片加签名。

修复步骤

  1. 调整快照捕获逻辑,不再依赖屏幕可视区域,而是基于图片实际缩放尺寸渲染完整内容
  2. 使用SwiftUI专用的视图快照方法,确保正确捕获缩放后的视图层级
  3. 修正保存后的视图状态,确保缩放回原比例时签名可见

修改后的代码

替换快照捕获相关代码

// 新的快照捕获函数,捕获完整缩放后的图片+签名
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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 08:58:10