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

Swift中如何在UIImage上绘制涂鸦线条并保存为新图片?

我完全懂你的需求——很多教程都是教在UIView上涂鸦,但你要的是在UIImage上画完直接保存成带线条的新图对吧?其实核心就是在UIImageView的基础上叠加绘制线条,最后把原图和所有涂鸦合成一张新的UIImage。下面给你一套完整的Swift实现方案,亲测可用:

核心思路
  1. 自定义继承自UIImageView的子类,开启用户交互来响应触摸事件
  2. 用数组记录所有绘制的线条信息(起点、终点、颜色、宽度),保证重绘时线条不会消失
  3. 在draw(_:)方法中遍历线条数组绘制所有涂鸦
  4. 合成图片时,先绘制原始图片,再叠加所有线条,生成最终的带涂鸦图片

1. 定义线条数据结构

首先我们需要一个结构体来存储单条线条的关键信息:

struct Stroke {
    let startPoint: CGPoint
    let endPoint: CGPoint
    let color: UIColor
    let lineWidth: CGFloat
}

2. 自定义涂鸦UIImageView子类

这个类会处理触摸事件、绘制线条,以及合成保存图片的逻辑:

class DoodleImageView: UIImageView {
    // 保存所有已绘制的线条
    private var strokes: [Stroke] = []
    // 记录上一个触摸点,用于绘制连续线条
    private var lastTouchPoint: CGPoint?
    
    // 可自定义的涂鸦样式
    var doodleColor: UIColor = .systemRed
    var doodleLineWidth: CGFloat = 3.0
    
    override init(image: UIImage?) {
        super.init(image: image)
        setupBasicConfig()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupBasicConfig()
    }
    
    private func setupBasicConfig() {
        // UIImageView默认关闭交互,必须开启才能响应触摸
        isUserInteractionEnabled = true
        // 设置背景色避免透明背景绘制异常
        backgroundColor = .white
    }
    
    // MARK: - 触摸事件处理
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        lastTouchPoint = touch.location(in: self)
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first, let lastPoint = lastTouchPoint else { return }
        let currentPoint = touch.location(in: self)
        
        // 添加一段新的线条到数组
        strokes.append(Stroke(
            startPoint: lastPoint,
            endPoint: currentPoint,
            color: doodleColor,
            lineWidth: doodleLineWidth
        ))
        
        // 更新上一个触摸点
        lastTouchPoint = currentPoint
        
        // 触发视图重绘
        setNeedsDisplay()
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        // 结束绘制,清空临时触摸点
        lastTouchPoint = nil
    }
    
    // MARK: - 绘制所有线条
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        guard let context = UIGraphicsGetCurrentContext() else { return }
        
        for stroke in strokes {
            stroke.color.setStroke()
            context.setLineWidth(stroke.lineWidth)
            // 设置圆角端点和拐角,让涂鸦更自然
            context.setLineCap(.round)
            context.setLineJoin(.round)
            
            context.move(to: stroke.startPoint)
            context.addLine(to: stroke.endPoint)
            context.strokePath()
        }
    }
    
    // MARK: - 合成并保存图片
    func saveDoodledImage() -> UIImage? {
        // 开启图片绘制上下文,适配屏幕分辨率
        UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.main.scale)
        
        // 先绘制原始图片
        image?.draw(in: bounds)
        
        // 再叠加绘制所有涂鸦线条
        guard let context = UIGraphicsGetCurrentContext() else {
            UIGraphicsEndImageContext()
            return nil
        }
        
        for stroke in strokes {
            stroke.color.setStroke()
            context.setLineWidth(stroke.lineWidth)
            context.setLineCap(.round)
            context.setLineJoin(.round)
            
            context.move(to: stroke.startPoint)
            context.addLine(to: stroke.endPoint)
            context.strokePath()
        }
        
        // 获取合成后的图片
        let finalImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        
        // 可选:保存到系统相册
        if let image = finalImage {
            UIImageWriteToSavedPhotosAlbum(image, self, #selector(imageSavedCallback(_:didFinishSavingWithError:contextInfo:)), nil)
        }
        
        return finalImage
    }
    
    // 相册保存回调
    @objc private func imageSavedCallback(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
        if let error = error {
            print("图片保存失败:\(error.localizedDescription)")
        } else {
            print("图片已成功保存到相册!")
        }
    }
    
    // 清空所有涂鸦
    func clearAllDoodles() {
        strokes.removeAll()
        setNeedsDisplay()
    }
}

3. 使用示例

在ViewController中快速集成这个涂鸦视图:

class ViewController: UIViewController {
    private var doodleView: DoodleImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 创建涂鸦视图并设置原图
        doodleView = DoodleImageView(image: UIImage(named: "your-image-name"))
        doodleView.frame = view.bounds
        doodleView.contentMode = .scaleAspectFit
        view.addSubview(doodleView)
        
        // 添加保存按钮
        let saveBtn = UIButton(type: .system)
        saveBtn.setTitle("保存涂鸦图片", for: .normal)
        saveBtn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
        saveBtn.frame = CGRect(x: 20, y: view.safeAreaInsets.top + 20, width: view.bounds.width - 40, height: 50)
        saveBtn.addTarget(self, action: #selector(saveButtonTapped), for: .touchUpInside)
        view.addSubview(saveBtn)
    }
    
    @objc private func saveButtonTapped() {
        _ = doodleView.saveDoodledImage()
    }
}

关键注意点
  • 必须开启isUserInteractionEnabled = true,否则UIImageView无法响应触摸事件
  • 保存线条数组是为了在视图重绘时(比如屏幕旋转)重新绘制所有线条,避免涂鸦消失
  • 合成图片时要先画原图再画线条,这样得到的是真正合并后的UIImage,而非视图截图

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 07:05:12