SwiftUI实现画板功能 | 来自缤纷多彩的灰

SwiftUI实现画板功能 @ WHlcj | 2023-08-25T19:50:11+08:00 | 4 分钟阅读

介绍两种swiftUI实现画板的方法,演示的PaintDemo在这里

本博客请一定配合PaintDemo食用~Demo的PaintDemoApp需要读者手动切换两种demo。PaintDemo中介绍的两种画板实现方法都只实现了基础功能、包括调整画笔粗细、画笔颜色、保存画板内容到手机相册、清空画板等。

两种画板具体属性调整的方法不同但都不难理解。PaintDemo中大部分地方已经给出注释,相信读者自行阅读代码理解更快。也因此,下文仅指出两种画板实现中相对难理解,需要说明的地方。

PencilKit

    PencilKit 是在2019年的 WWDC 上推出的。该框架是为了支持苹果的触控笔 Apple Pencil,并为开发者提供一个简单而强大的工具,用于在 iOS 和 iPadOS 上创建绘图和手写笔记应用程序。它提供了多种绘图工具、手写识别、橡皮擦工具以及手势和手写操作的支持。PencilKit 针对大规模手写和绘图场景进行了优化,与其他框架无缝集成,并且具有高性能和优化。自发布以来,PencilKit 已经成为开发者们创建涂鸦、手写输入和其他与笔迹相关的应用的主要选择之一。

    简单来说PencilKit可以实现iPad自带画图工具的所有功能。Demo中只是简单实现了部分基础功能,代码都已经加上注释,有swiftUI基础应该都能看懂,就不多介绍啦。下面就部分细节进行说明。

    Demo演示文件中的DrawingBoard就是自定义的Pencilkit的画板。常用参数和注释信息已在Demo中给出。makeUIView(context: Context)和updateUIView(_ uiView: UIViewType, context: Context)都是协议UIViewRepresentable的必须实现方法。需要说明的是makeUIView一经创建,绑定的canvas就无法更改,只能修改绑定的canvas的属性(也就是画笔粗细、颜色、工具)。

1
2
3
4
5
6
    /// Creates the view object and configures its initial state.
    func makeUIView(context: Context) -> PKCanvasView {
        canvas.drawingPolicy = .anyInput
        canvas.tool = isDrawing ? ink : eraser
        return canvas
    }

PKCanvasView提供了ink(.pencil、.pen、.marker)画图工具和eraser擦除工具,因为.pencil、.pen、.marker和eraser不是同一类。所以自定义的DrawingBoard需要绑定一个Bool值(Demo中绑定的是isDrawing)来切换ink和eraser两种工具。

1
2
3
4
5
    /// Updates the state of the specified view with new information from SwiftUI.
    func updateUIView(_ uiView: UIViewType, context: Context) {
        uiView.tool = isDrawing ? ink : eraser
    }
}

同时由于Demo中的ink是个计算属性,跟颜色、粗细、ink类型都有关系可以实时检测改变,但是工具只是画图工具跟isDrawing没有联系。所以在updateUIView(_ uiView: UIViewType, context: Context)方法中来检测isDrawing的变化来切换ink和eraser。

Path

    在SwiftUI框架中,Path 是一个用于绘制和操作矢量图形路径的类。 Path 提供了一个强大而灵活的 API,你可以通过添加直线、曲线、椭圆、矩形等基本图形元素来构造路径和操作矢量图形路径,从而实现各种自定义图形和动画效果。

    通过Path来实现画板的思想是,先自定义每一段线段的颜色、粗细、样式。(Demo中没有设置画笔样式)

1
2
3
4
5
6
/// 画图线段
struct DrawingLine {
    var point = [CGPoint]()
    var lineWidth = 1.0
    var color = Color.green
}

通过拖拽手势DragGesture的.onChanged和.onEnded可以轻松获取画图的路径数据,再通过addLine()连"点"成线。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
        .gesture(
            DragGesture(minimumDistance: 0, coordinateSpace: .local)
                .onChanged { value in
                    let newLocation = value.location
                    // 开始画画时,记录起点位置index
                    if !startAddLine {
                        startIndex = lines.endIndex
                        pathes.append(startIndex)
                        startAddLine = true
                    } else {
                        currentLine.point.append(newLocation)
                        lines.append(currentLine)
                    }
                }
                .onEnded { value in
                    lines.append(currentLine)
                    currentLine = DrawingLine(lineWidth: lineWidth, color: currentColor)
                    // 结束画画时,记录终点位置index
                    endIndex = lines.endIndex
                    pathes.append(endIndex)
                    startAddLine = false
                }
        )

同时记录下每次画画起点和终点index的位置,通过在lines数组中删掉对应的范围,即可实现画画回退效果。

1
2
3
4
5
6
7
8
9
    /// 清除最后一次画笔的笔迹
    private func removeLastPath() {
        if !pathes.isEmpty {
            let startIndex = pathes[pathes.count - 2] + 1
            let endIndex = pathes[pathes.count - 1]
            lines.removeLast(endIndex-startIndex)
            pathes.removeLast(2)
        }
    }

需要注意的是,在PathDrawingDemo中我实现的屏幕截图方式跟PencilKitDemo的截图方式有些许不同,这种截图方式要求View必须有明确的bounds所以代码中通过.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height * 4/5)圈定了画板的长宽,读者可以自行对比参考。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    /// 屏幕截图
    func snapshot() {
        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)
        
        let image = renderer.image { _ in
            view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
        }
        UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
    }

    个人角度来说,如果只需要实现画板的基础功能,那么首推PencilKit的方法,实现简单且好用。但是如果你需要实现复杂的画画效果,比如实现自定义的画笔路径或者想自定义实现多种画图工具,那么可以试试Path,Path虽难一点,麻烦一点但是无上限!读者可自行选择。