[iOS] SwiftUIでお絵かきしたい

2021.09.03

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。きんくまです。

SwiftUIでお絵かきしてみたいです。
今までFlashとかhtmlのCanvasとかで同じようなのを作ったことがあるので、SwiftUIでも作れるのか試してみました。

ビットマップではなくて、ベクターのお絵かきになります。

できたもの

Model

1ストロークごとの開始地点の座標と、線を結ぶための複数の座標があれば描けそうです。
ForEachで回したいのでIdentifiableに準拠するためにidを入れてあります。

struct DrawLine: Identifiable {
    static var idCount: Int = 0
    var id: String
    var points: [CGPoint]
    
    static func makeDrawLine(points: [CGPoint]) -> DrawLine {
        let line = DrawLine(id: "\(DrawLine.idCount)", points: points)
        DrawLine.idCount += 1
        return line
    }
}

View

考え方としては、ドラッグをしながら座標を記録。ドラッグが終わったら、その座標をアーカイブとして保存。
描画も、ドラッグ中のものと、アーカイブしているものをそれぞれ分けて描画してあげれば、実装も簡単そうです。

struct ContentView: View {
    // すでに描いたLine
    @State private var lines: [DrawLine] = []
    // いまドラッグ中のLine
    @State private var currentLine: DrawLine?
    
    var body: some View {
        VStack {
            // リセットボタン
            Button(action: {
                lines = []
            }, label: {
                Text("Clear")
            })
            ZStack {
                // Canvas部分
                Rectangle()
                    .fill(Color.white)
                    .border(Color.black, width: 1)
                    .gesture(
                        DragGesture(minimumDistance: 0, coordinateSpace: .local)
                            .onChanged({ value in
                                if currentLine == nil {
                                    currentLine = DrawLine.makeDrawLine(points: [])
                                }
                                guard var line = currentLine else { return }
                                line.points.append(value.location)
                                currentLine = line
                            })
                            .onEnded({ value in
                                guard var line = currentLine else { return }
                                line.points.append(value.location)
                                lines.append(line)
                                // リセット
                                currentLine = nil
                            })
                    )
                
                // 追加ずみのLineの描画
                ForEach(lines) { line in
                    Path { path in
                        path.addLines(line.points)
                    }.stroke(Color.red, lineWidth: 1)
                }.clipped()
                
                // ドラッグ中のLineの描画
                Path { path in
                    guard let line = currentLine else { return }
                    path.addLines(line.points)
                }.stroke(Color.red, lineWidth: 1)
                .clipped()
                
            }.padding(20)
        }
    }
}

結構簡単に実装できました。
出てくる要望としては、とかでしょうか。

  1. 線ごとに色を変えたい
  2. 線ごとに太さを変えたい
  3. アンドゥを入れたい

1と2は、以下で実装できそうです。

  • Modelに色と太さのプロパティを追加
  • 色と太さの切り替えパネルを作る
  • 切り替えパネルの状態を1ストロークごとに記録
  • パスを描画するときに色と太さを反映

3は lines というプロパティの一番後ろデータを1つずつ削除していけばできますね。

まとめ

手軽に描画処理ができるのは良いなと思いました。
ではでは。