SwiftUI如何在图片上绘制后将绘图与原图合并保存
核心问题原因
- 调用
snapshot()方法时是重新生成了imageDrawingView实例,内部的CanvasView是全新对象,你之前手绘的路径存在旧的屏幕渲染的CanvasView实例里,新实例路径为空,自然不会有手绘内容 CanvasView没有固定尺寸约束,快照时生成的画布大小和实际显示大小不一致@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
相关产品推荐
相关产品推荐

