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

SwiftUI如何在图片上绘制后将绘图与原图合并保存

核心问题原因

  1. 调用snapshot()方法时是重新生成了imageDrawingView实例,内部的CanvasView是全新对象,你之前手绘的路径存在旧的屏幕渲染的CanvasView实例里,新实例路径为空,自然不会有手绘内容
  2. CanvasView没有固定尺寸约束,快照时生成的画布大小和实际显示大小不一致
  3. @Published属性放在UIView子类中无效,该属性仅对遵守ObservableObject协议的类生效

修复方案

1. 调整路径数据的存储位置,把路径提到上层视图管理

修改CanvasView,让它接收外部传入的路径数组:

import Foundation
import UIKit

class CanvasView: UIView {
    var paths = [Path]() {
        didSet {
            setNeedsDisplay()
        }
    }
    
    override func draw(_ rect: CGRect){
        super.draw(rect)
        
        guard let context = UIGraphicsGetCurrentContext() else { return }
        
        paths.forEach { path in
            switch(path.type) {
            case .move:
                context.move(to: path.point)
                break
            case .line:
                context.addLine(to: path.point)
                break
            }
        }
        
        context.setLineWidth(10)
        context.setStrokeColor(UIColor.black.cgColor) // 建议显式设置笔触颜色
        context.setLineCap(.round)
        context.strokePath()
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let point = touches.first?.location(in: self) else { return }
        paths.append(Path(type: .move, point: point))
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let point = touches.first?.location(in: self) else { return }
        paths.append(Path(type: .line, point: point))
    }
}

struct Path {
    let type: PathType
    let point: CGPoint
    
    init(type: PathType, point: CGPoint) {
        self.type = type
        self.point = point
    }
    
    enum PathType {
        case move
        case line
    }
}

2. 修改CanvasViewWrapper,绑定外部路径

import SwiftUI

struct CanvasViewWrapper : UIViewRepresentable {
    @Binding var paths: [Path]
    
    func makeUIView(context: Context) -> CanvasView {
        let view = CanvasView()
        view.backgroundColor = .clear
        return view
    }
    
    func updateUIView(_ uiView: CanvasView, context: Context) {
        uiView.paths = paths
        // 同步用户操作产生的新路径回上层
        if uiView.paths != paths {
            paths = uiView.paths
        }
    }
}

3. 调整ContentView,存储路径,快照使用当前渲染的视图

如果你的App支持iOS16及以上,推荐使用官方的ImageRenderer进行快照,稳定性更高:

import SwiftUI

struct ContentView: View {
    @State private var snapshotImage: UIImage? = nil
    @State private var paths: [Path] = [] // 存储手绘路径
    @State private var viewSize: CGSize = .zero // 存储底图实际大小
    
    var imageView: some View {
        Image("Test")
            .resizable()
            .scaledToFit()
            .overlay(
                // 获取底图实际渲染尺寸
                GeometryReader { proxy in
                    Color.clear.preference(key: SizePreferenceKey.self, value: proxy.size)
                }
            )
            .onPreferenceChange(SizePreferenceKey.self) { size in
                viewSize = size
            }
        //EmptyView() <- Uncomment and comment out the line above to just use an empty View as an image
    }

    var imageDrawingView: some View {
        ZStack {
            if let image = snapshotImage {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
                CanvasViewWrapper(paths: $paths)
                    .frame(width: viewSize.width, height: viewSize.height) // 画布和底图大小一致
            }
        }
    }
    
    var body: some View {
        if snapshotImage != nil {
            VStack {
                imageDrawingView
                    .id("DrawingView") // 固定标识确保是同一个渲染实例
                Button("Save Image") {
                    // iOS16+ 用ImageRenderer
                    let renderer = ImageRenderer(content: imageDrawingView)
                    guard let imageWithDrawing = renderer.uiImage else { return }
                    UIImageWriteToSavedPhotosAlbum(imageWithDrawing, nil, nil, nil)
                    
                    // 如果要兼容更低版本,用原来的快照方法也可以,因为现在路径已经存在上层,重新生成实例也不会丢失
//                    let imageWithDrawing = imageDrawingView.snapshot()
//                    UIImageWriteToSavedPhotosAlbum(imageWithDrawing, nil, nil, nil)
                }
            }
        } else {
            VStack {
                imageView
                Button("Take ScreenShot") {
                    self.snapshotImage = imageView.snapshot()
                }
            }
        }
    }
}

// 新增尺寸获取的PreferenceKey
struct SizePreferenceKey: PreferenceKey {
    static var defaultValue: CGSize = .zero
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        value = nextValue()
    }
}

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)
        }
    }
}

4. 额外配置

记得在项目的Info.plist中添加相册写入权限描述:NSPhotoLibraryAddUsageDescription,否则保存到相册会崩溃。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.29 00:15:02