お絵かきソフトの実装 〜 タッチ系イベント処理とベジェ曲線の描画

ここではタッチ関連の基本的なイベント処理と、ビューへの描画の例として簡単なお絵かきソフトを作りましょう。

ここで作るプログラムはどんなもの?

ここで作るアプリは次のようなものです。

中央のグレーの部分がお絵かきエリアです。ここにフリーハンドで書き込めます。スクリーンショットの Hello world! の文字や下線を描くときは Apple Pencil を使いました。

上側は左から、クリアボタン、Undo (やり直し) ボタン、描画色を選択するコントロールです。

線の描画時に画面へのタッチ、移動といった、タッチ関連のイベントを処理します。

タッチした場所に線を描くときには、UIView への直接描画を行います。

もっと滑らかな線を描くには Apple の開発者用のサンプルプログラム Speed Sketch が参考になります。上のスクリーンショット程度ではスムーズさの要件を満たさない、という場合は、そちらをみてください。

タッチ関連のイベント処理

さっそくタッチ関連のイベントをみて行きましょう。ここではタッチの基本イベントである次の三つを処理します。

  • touchesBegan - タッチの開始。タッチが検出された時に UIKit がこのメソッドをコールバックします。
  • touchesMove - タッチの移動。タッチの場所や力に変更があった時に UIKit がこのメソッドをコールバックします。
  • touchesEnded - タッチの終了。タッチが終了した時 (指や Apple Pencil が画面から離れた時) に UIKit がこのメソッドをコールバックします。
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    // 開始
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    // 移動
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    // 終了
}

パラメータとして渡される UITouch オブジェクトから場所などの情報が取得できます。具体的な利用例は下のコード例をみてください。

線の描画

UIView クラスのビュー領域が再描画が必要になった時には、draw() メソッドがコールバックされます。 画面が表示される際にシステムがコンテンツを描画するときに draw を自動的に呼び出します。

その他、setNeedsDisplay() を呼ぶことによって、システムに再描画するように通知することもできます。これによって、フレームワークが draw() を呼び出します。

線を描画するには UIKit で定義されている UIBezierPath を使います。これはベジェ曲線を描画します。

お絵かきソフトの開発

UI の作成

インターフェイスビルダーで UI のコンポーネントを配置しましょう。

今回は配置場所やサイズはあまり大事ではないので、適当に配置して大丈夫です。

アウトレットとアクションは次の通りです。

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var drawView: DrawView!
    @IBOutlet weak var segmentedControl: UISegmentedControl!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func clearTapped(_ sender: Any) {
    }
    
    @IBAction func undoTapped(_ sender: Any) {
    }
    
    @IBAction func colorChanged(_ sender: Any) {
    }
}

UIView のアウトレットを drawView、セグメンテッドコントロールのアウトレットを segmentedControl としています。

Clear ボタンのアクションは clearTapped。 Undo ボタンのアクションは undoTapped。セグメンテッドコントロールのアクションは colorChangedです。

中身は後で実装します。

データクラスとビュークラスの作成

次の内容を Drawing.swift という名前で保存します。

import CoreGraphics
import UIKit

struct Drawing {
    var color = UIColor.black
    var points = [CGPoint]()
}

この Drawing データ一つ一つが、一度のタッチで作成される線一本の色と座標を保持します。

次の内容を DrawView.swift という名前で保存します。

タッチイベントの処理や描画など、今回の目的のコードが詰まっています。

import UIKit

class DrawView: UIView {
    
    var currentDrawing: Drawing?
    var finishedDrawings = [Drawing]()
    var currentColor = UIColor.black
    
    override func draw(_ rect: CGRect) {
        for drawing in finishedDrawings {
            drawing.color.setStroke()
            stroke(drawing: drawing)
        }
        
        if let drawing = currentDrawing {
            drawing.color.setStroke()
            stroke(drawing: drawing)
        }
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first!
        let location = touch.location(in: self)
        currentDrawing = Drawing()
        currentDrawing?.color = currentColor
        currentDrawing?.points.append(location)
        setNeedsDisplay()
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first!
        let location = touch.location(in: self)
                
        currentDrawing?.points.append(location)
        
        setNeedsDisplay()
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        if var drawing = currentDrawing {
            let touch = touches.first!
            let location = touch.location(in: self)
            drawing.points.append(location)
            finishedDrawings.append(drawing)
        }
        currentDrawing = nil
        setNeedsDisplay()
    }
    
    func clear() {
        finishedDrawings.removeAll()
        setNeedsDisplay()
    }
    
    func undo() {
        if finishedDrawings.count == 0 {
            return
        }
        finishedDrawings.remove(at: finishedDrawings.count - 1)
        setNeedsDisplay()
    }
    
    func setDrawingColor(color : UIColor){
        currentColor = color
    }
    
    func stroke(drawing: Drawing) {
        let path = UIBezierPath()
        path.lineWidth = 10.0
        path.lineCapStyle = .round
        path.lineJoinStyle = .round
        
        let begin = drawing.points[0];
        path.move(to: begin)
        
        if drawing.points.count > 1 {
            for i in 1...(drawing.points.count - 1) {
                let end = drawing.points[i]
                path.addLine(to: end)
            }
        }
        path.stroke()
    }
}

currentDrawing メンバーは、現在ユーザーが描いている最中の線を表しています。touchesBegan() で新しいインスタンスが作成され、 touchesMoved で座標が追加され、touchesEndedfinishedDrawings リストに追加されます。

finishedDrawings リストは、Drawing オブジェクトのリストです。描き終わったらこのリストにデータが追加されます。

描画用データを更新したら setNeedsDisplay() を呼び出します。これによって、システムが draw() をコールバックし、draw() で更新されたデータを使って線を再描画します。

currentColor は現在選択中の描画色を保持しています。

stroke() メソッドは Drawing オブジェクトを一つ受け取って描画するだけのメソッドです。

ここではグラフィックコンテキストを直接取得して使うのではなく UIBezierPath() を使って、線を描画しています。

グラフィックスの低レベルにあるのは Core Graphics フレームワークです。Core Graphics 系のタイプには CG という接頭辞がついています。例えば CGPoint などです。

一方、UIKit は Core Graphics を利用したハイレベルのフレームワークです。接頭辞は UI です。例えば UIColor です。

UIKit ではコアグラフィックスの詳細をラップして、簡単に使えるようになっています。

その他、DrawView クラスでは clear()undo()setDrawingColor() などのカスタムメソッドを実装しています。 これらはビューコントローラから呼び出して使います。

この DrawView クラスを、ビューのカスタムクラスとして設定します。

これでタッチ関連のイベントに応じて、線を描画する DrawView クラスが使える状態になりました。

ビューコントローラの実装

ViewController.swift を編集します。

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var drawView: DrawView!
    @IBOutlet weak var segmentedControl: UISegmentedControl!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        segmentedControl.selectedSegmentIndex = 0
    }

    @IBAction func clearTapped(_ sender: Any) {
        drawView.clear()
    }
    
    @IBAction func undoTapped(_ sender: Any) {
        drawView.undo()
    }
    
    @IBAction func colorChanged(_ sender: Any) {
        var c = UIColor.black
        switch segmentedControl.selectedSegmentIndex {
        case 1:
            c = UIColor.blue
            break
        case 2:
            c = UIColor.red
            break
        default:
            break
        }
        drawView.setDrawingColor(color: c)
    }
}

Clear ボタンをタップしたら、DrawView クラスの clear() メソッドを呼び出します。 そこで描画データリストをクリアして、画面を再描画することで線を消します。

Undo ボタンでは DrawView クラスの undo() メソッドを呼び、描画データリストの最後尾からデータを一つ削除し、再描画することで最後に描いた線を消します。

描画色の指定では、セグメンテッドコントロールで選んだ色を DrawView クラスの setDrawingColor() メソッドに渡すことで描画色を設定します。

以上で、冒頭のような簡単なお絵かきソフトの実装方法を説明しました。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Swift による iOS 開発入門