iOS標準機能の良いお絵描きアプリを目指して・・・

# はじめに
こんにちは:leaves:
iOS標準UIViewの`draw(_ rect: CGRect)`等を利用した**お絵描きアプリ**について書いてみたいと思います。

昔から**お絵描きアプリ**はストアにも多く出ていますし、実際に機能などを頑張って開発してリリースしていても
なかなかDL数は伸びないもので、悲しくなったりします。
そんなお涙頂戴なスタートですが、私の好きな**Qiita**に記事を書いて、私なりの頑張りを挙げてみたいと思います。見ていただけたら本当に嬉しい限りです。

# iOSのお絵描きアプリには素晴らしいものがたくさん
Pixiv様の「[Pixiv Sketch](https://itunes.apple.com/us/app/pixiv-sketch/id991334925?mt=8)」
> [![](https://qiita-image-store.s3.amazonaws.com/0/128250/9a79d25d-135a-f2c1-0ad2-0e959cf46ea9.png)](https://itunes.apple.com/us/app/pixiv-sketch/id991334925?mt=8) 

urecy様の「[Let's Draw お絵描きアプリ無料版 - お絵かき&写真に落書き](https://itunes.apple.com/jp/app/lets-draw-%E3%81%8A%E7%B5%B5%E6%8F%8F%E3%81%8D%E3%82%A2%E3%83%97%E3%83%AA%E7%84%A1%E6%96%99%E7%89%88-%E3%81%8A%E7%B5%B5%E3%81%8B%E3%81%8D-%E5%86%99%E7%9C%9F%E3%81%AB%E8%90%BD%E6%9B%B8%E3%81%8D/id629814709?mt=8)」
> [![](https://qiita-image-store.s3.amazonaws.com/0/128250/7465e0dd-ee16-7945-a55e-9d68db560008.png)](https://itunes.apple.com/jp/app/lets-draw-%E3%81%8A%E7%B5%B5%E6%8F%8F%E3%81%8D%E3%82%A2%E3%83%97%E3%83%AA%E7%84%A1%E6%96%99%E7%89%88-%E3%81%8A%E7%B5%B5%E3%81%8B%E3%81%8D-%E5%86%99%E7%9C%9F%E3%81%AB%E8%90%BD%E6%9B%B8%E3%81%8D/id629814709?mt=8) 

CELSYS様の「[kakooyo! – 楽しく描ける無料お絵かきアプリ](https://itunes.apple.com/jp/app/o-huikaki-toku!kakooyo!/id932223997?mt=8&at=&ct=&ign-mpt=uo%3D6)」
> [![](https://qiita-image-store.s3.amazonaws.com/0/128250/29bf0490-05f0-c1cf-8598-e85f03c400df.png)](https://itunes.apple.com/jp/app/o-huikaki-toku!kakooyo!/id932223997?mt=8&at=&ct=&ign-mpt=uo%3D6)

MediBang様の「[メディバンペイント 漫画イラスト・簡単お絵かきアプリ](https://itunes.apple.com/jp/app/medibang-paint-mini-draw-easily/id1031158715)」
> [![](https://qiita-image-store.s3.amazonaws.com/0/128250/ca6e1a09-3ed2-644a-d3ef-e4d6df8c8c08.png)](https://itunes.apple.com/jp/app/medibang-paint-mini-draw-easily/id1031158715)

iOS標準の機能でどれだけ作られてるかは分かりませんが、
どれも機能が強力で、自分の出る幕が無いなと思ってしまいます(;ω;)

# iOSのDrawアプリの基本
検索するといろいろ出てくるのですが、私の知っている中では以下のものがあります。

- UIViewの`draw(_ rect: CGRect)`でUIBezierPathを描画する方法
- UIImageViewのimageプロパティを入れ替えて描画する方法
- UIViewのlayerの下にCALayerを追加して描画する方法

描画速度などについて**marty-suzuki**様がQiita記事でまとめてありましたので
とても参考になります。
> [【iOS】drawRect、CALayer、UIImageViewで描画速度を検証してみた](http://qiita.com/marty-suzuki/items/a28269ee39b6e0ec0830)

## UIViewのdraw(_ rect: CGRect)でUIBezierPathを描画する方法

```Swift
import UIKit

final class DrawView: UIView {
    
    var penColor = UIColor.black
    var penSize: CGFloat = 6.0
    private var path: UIBezierPath!
    private var lastDrawImage: UIImage!
    
    override func draw(_ rect: CGRect) {
        lastDrawImage?.draw(at: CGPoint.zero)
        penColor.setStroke()
        path?.stroke()
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let currentPoint = touches.first!.location(in: self)
        path = UIBezierPath()
        path?.lineWidth = penSize
        path?.lineCapStyle = CGLineCap.round
        path?.lineJoinStyle = CGLineJoin.round
        path?.move(to: currentPoint)
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let currentPoint = touches.first!.location(in: self)
        path?.addLine(to: currentPoint)
        setNeedsDisplay()
    }
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        let currentPoint = touches.first!.location(in: self)
        path.addLine(to: currentPoint)
        lastDrawImage = snapShot()
        setNeedsDisplay()
    }
    
    func snapShot() -> UIImage {
        UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
        lastDrawImage?.draw(at: CGPoint.zero)
        penColor.setStroke()
        path?.stroke()
        let image = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        return image
    }
}
```

## UIImageViewのimageプロパティを利用した方法

```Swift
import UIKit

final class DrawView: UIImageView {
    
    var penColor = UIColor.black
    var penSize: CGFloat = 6.0
    private var path: UIBezierPath!
    private var lastDrawImage: UIImage!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.isUserInteractionEnabled = true
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.isUserInteractionEnabled = true
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let currentPoint = touches.first!.location(in: self)
        path = UIBezierPath()
        path?.lineWidth = penSize
        path?.lineCapStyle = CGLineCap.round
        path?.lineJoinStyle = CGLineJoin.round
        path?.move(to: currentPoint)
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let currentPoint = touches.first!.location(in: self)
        path?.addLine(to: currentPoint)
        drawLine()
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        let currentPoint = touches.first!.location(in: self)
        path.addLine(to: currentPoint)
        drawLine()
        lastDrawImage = self.image
    }
    
    func drawLine() {
        UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
        lastDrawImage?.draw(at: CGPoint.zero)
        penColor.setStroke()
        path?.stroke()
        self.image = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
    }
}
```

## UIViewのlayerの下にCALayerを追加して描画する方法
```Swift
import UIKit

final class DrawView: UIView {
    
    var penColor = UIColor.black
    var penSize: CGFloat = 6.0
    private var path = UIBezierPath()
    private var drawLayer: CAShapeLayer!
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        drawLayer = CAShapeLayer()
        drawLayer.frame = bounds
        drawLayer.strokeColor = penColor.cgColor
        drawLayer.fillColor = UIColor.clear.cgColor
        drawLayer.lineWidth = penSize
        drawLayer.lineCap = kCALineCapRound
        drawLayer.lineJoin = kCALineCapRound
        layer.addSublayer(drawLayer)
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let currentPoint = touches.first!.location(in: self)
        path.move(to: currentPoint)
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let currentPoint = touches.first!.location(in: self)
        path.addLine(to: currentPoint)
        drawLayer.path = path.cgPath
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        let currentPoint = touches.first!.location(in: self)
        path.addLine(to: currentPoint)
        drawLayer.path = path.cgPath
    }
}
```

# 次々に浮上する描画に関する問題たち
私が作成したアプリでは**UIImageView**を利用しました。
実装をしていくにあたって様々な問題と出会いました。それを以下に書いてみたいと思います。
以下では、UIImageViewの方法のコードを書き直します。

## 1.線がカクカクする問題
UIBezierPathをタッチイベントのまま追加して描画をすると、画面更新のフレームレートの影響によって線がカクついてしまいます。(ゆっくり描けば大丈夫ですが...)

![hikaku.png](https://qiita-image-store.s3.amazonaws.com/0/128250/edf05e88-7b72-c618-8321-137c5e17caf3.png)


このカクカク問題は2次ベジエ曲線を利用することにより解決できそうです。
> 参考([クラスメソッド様](https://classmethod.jp/))
[[iOSアプリ開発] ちょっとなめらかな線でお絵かきしてみる](http://dev.classmethod.jp/smartphone/iphone/ios-little-smooth-line-drawing/)

```Swift
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    let currentPoint = touches.first!.location(in: self)
    let previousPoint = touches.first!.previousLocation(in: self)
    let middlePoint = CGPoint(x: (currentPoint.x + previousPoint.x) * 0.5, y: (currentPoint.y + previousPoint.y) * 0.5)
    path.addQuadCurve(to: middlePoint, controlPoint: previousPoint)
    drawLine()
}
```

## 2.線が欠けてしまう問題
上の記事は、もちろん素晴らしい実装ですが、線を垂直に向けて同じ場所に戻るように線を引いた時に、線の末端が欠けてしまう現象があります。

<img width="312" alt="スクリーンショット 2017-04-22 14.09.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/128250/797e7da6-2d7c-c42a-312e-134dc5ec2a6a.png">

この問題を解決するために`addCurve(to endPoint: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint)`を利用した**3次ベジェ曲線**について私は調べました。

> 参考(stackoverflow)
http://stackoverflow.com/questions/33538962/differences-between-addcurvetopoint-and-addquadcurvetopoint-drawing-smooth-curve?rq=1

```Swift
import UIKit

class DrawView: UIImageView {
    
    var penColor = UIColor.black
    var penSize: CGFloat = 6
    private var path: UIBezierPath?
    private var lastDrawImage: UIImage?
    
    private var points: [CGPoint] = []
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.isUserInteractionEnabled = true
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.isUserInteractionEnabled = true
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let currentPoint = touches.first!.location(in: self)
        path = UIBezierPath()
        path?.lineWidth = penSize
        path?.lineCapStyle = .round
        path?.lineJoinStyle = .round
        points = [currentPoint]
        path?.move(to: points[0])
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let currentPoint = touches.first!.location(in: self)
        points.append(currentPoint)
        if points.count == 5 {
            points[3] = CGPoint(x: (points[2].x + points[4].x) * 0.5, y: (points[2].y + points[4].y) * 0.5)
            path?.move(to: points[0])
            path?.addCurve(to: points[3], controlPoint1: points[1], controlPoint2: points[2])
            points = [points[3], points[4]]
        }
        drawLine()
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        drawLine()
        lastDrawImage = self.image
        path?.removeAllPoints()
    }
    
    func drawLine() {
        UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
        lastDrawImage?.draw(at: CGPoint.zero)
        penColor.setStroke()
        path?.stroke()
        self.image = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
    }
}
```

**touchMoved**で、入ってきたポイントを数えて3次ベジエ曲線を描いています。
しかし、タッチイベントで得たポイントをコントロールポイントとして利用しているので、かなり描画に遅れがあり、指を素早く動かすとモッサリ線が追いついて行ったりと描画が安定しません。(※3に続く)

## 3.描画が遅れてしまう問題
**左側が3次ベジエ曲線**で**右側が2次ベジエ曲線**です。
同じくらいの指の速さで描いていますが、分かりづらくてごめんなさい(>人<)
※実際は左側の**3次ベジエ曲線**はもっと描きづらい感じがします
![heby.gif](https://qiita-image-store.s3.amazonaws.com/0/128250/4ecfa395-ec00-01bf-bf22-a8a12448b8ff.gif)![light.gif](https://qiita-image-store.s3.amazonaws.com/0/128250/1f5fe0e3-09f5-c186-fe29-8ef8dd29a759.gif)

描きづらい問題を解決するために、**2つのUIBezierPathを合成して描画するという方法**を見つけました。

> 参考(stackoverflow)
https://stackoverflow.com/questions/34995799/removing-lagging-latency-in-drawing-uibezierpath-smooth-lines-in-swift?noredirect=1&lq=1

```Swift
import UIKit

final class DrawView: UIImageView {
    
    var penColor = UIColor.black
    var penSize: CGFloat = 6
    private var path: UIBezierPath!
    private var lastDrawImage: UIImage?
    
    private var points = [CGPoint]()
    private var temporaryPath: UIBezierPath!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        isUserInteractionEnabled = true
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        isUserInteractionEnabled = true
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let currentPoint = touches.first!.location(in: self)
        path = UIBezierPath()
        path?.lineWidth = penSize
        path?.lineCapStyle = CGLineCap.round
        path?.lineJoinStyle = CGLineJoin.round
        path?.move(to: currentPoint)
        points = [currentPoint]
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let currentPoint = touches.first!.location(in: self)
        points.append(currentPoint)
        if points.count == 2 {
            temporaryPath = UIBezierPath()
            temporaryPath?.lineWidth = penSize
            temporaryPath?.lineCapStyle = .round
            temporaryPath?.lineJoinStyle = .round
            temporaryPath?.move(to: points[0])
            temporaryPath?.addLine(to: points[1])
            image = drawLine()
        }else if points.count == 3 {
            temporaryPath = UIBezierPath()
            temporaryPath?.lineWidth = penSize
            temporaryPath?.lineCapStyle = .round
            temporaryPath?.lineJoinStyle = .round
            temporaryPath?.move(to: points[0])
            temporaryPath?.addQuadCurve(to: points[2], controlPoint: points[1])
            image = drawLine()
        }else if points.count == 4 {
            temporaryPath = UIBezierPath()
            temporaryPath?.lineWidth = penSize
            temporaryPath?.lineCapStyle = .round
            temporaryPath?.lineJoinStyle = .round
            temporaryPath?.move(to: points[0])
            temporaryPath?.addCurve(to: points[3], controlPoint1: points[1], controlPoint2: points[2])
            image = drawLine()
        }else if points.count == 5 {
            points[3] = CGPoint(x: (points[2].x + points[4].x) * 0.5, y: (points[2].y + points[4].y) * 0.5)
            path?.move(to: points[0])
            path?.addCurve(to: points[3], controlPoint1: points[1], controlPoint2: points[2])
            points = [points[3], points[4]]
            image = drawLine()
        }
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        image = drawLine()
        lastDrawImage = image
        temporaryPath = nil
    }
    
    func drawLine() -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
        lastDrawImage?.draw(at: CGPoint.zero)
        penColor.setStroke()
        path?.stroke()
        temporaryPath?.stroke()
        let capturedImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return capturedImage
    }
}
```

## 4.重くなる問題
指でなぞっている間、1つのPathに何度も移動する座標の情報や直線を引く情報が追加されていくので、描いている間に処理が重くなってしまいます。
そこで、描いている一定の間隔でPathを画像化すれば解決できそうと考えてみました。
※以下6にまとめて記述
## 5.タッチからすぐ指を離して点が描画されない問題 
Pathを画像化して軽くするためにtouchMovedで`move(to:)`をしているので、
touchEndedで容易に`addLine(to:)`できない(無条件に行うと線が2重に書かれてしまう)
のでフラグで調節することにします。
※以下6にまとめて記述
## 6.線が2重に描かれてしまう問題
3の問題の解決をしたら今度はこの問題が浮上してしまいました。

![double.png](https://qiita-image-store.s3.amazonaws.com/0/128250/14abacb8-f7e4-3fd1-a61b-78611fa997c6.png)

ここら辺になってくると、レベルが高く、私自身も挙動を完全に理解しているとは言えないのですが、
重くなってしまう問題や点が描画されない問題は解決してみました。

> 参考(stackoverflow)
https://stackoverflow.com/questions/35067811/removing-lagging-latency-during-continuous-period-of-drawing-uibezierpath-in-swi

```Swift
import UIKit

final class DrawView: UIImageView {
    
    var penColor = UIColor.black
    var penSize: CGFloat = 6
    private var path: UIBezierPath!
    private var lastDrawImage: UIImage?
    
    private var temporaryPath: UIBezierPath!
    private var points = [CGPoint]()
    
    private var pointCount = 0
    private var snapshotImage: UIImage?
    
    private var isCallTouchMoved = false
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        isUserInteractionEnabled = true
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        isUserInteractionEnabled = true
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let currentPoint = touches.first!.location(in: self)
        path = UIBezierPath()
        path?.lineWidth = penSize
        path?.lineCapStyle = CGLineCap.round
        path?.lineJoinStyle = CGLineJoin.round
        path?.move(to: currentPoint)
        points = [currentPoint]
        pointCount = 0
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        isCallTouchMoved = true
        pointCount += 1
        let currentPoint = touches.first!.location(in: self)
        points.append(currentPoint)
        if points.count == 2 {
            temporaryPath = UIBezierPath()
            temporaryPath?.lineWidth = penSize
            temporaryPath?.lineCapStyle = .round
            temporaryPath?.lineJoinStyle = .round
            temporaryPath?.move(to: points[0])
            temporaryPath?.addLine(to: points[1])
            image = drawLine()
        }else if points.count == 3 {
            temporaryPath = UIBezierPath()
            temporaryPath?.lineWidth = penSize
            temporaryPath?.lineCapStyle = .round
            temporaryPath?.lineJoinStyle = .round
            temporaryPath?.move(to: points[0])
            temporaryPath?.addQuadCurve(to: points[2], controlPoint: points[1])
            image = drawLine()
        }else if points.count == 4 {
            temporaryPath = UIBezierPath()
            temporaryPath?.lineWidth = penSize
            temporaryPath?.lineCapStyle = .round
            temporaryPath?.lineJoinStyle = .round
            temporaryPath?.move(to: points[0])
            temporaryPath?.addCurve(to: points[3], controlPoint1: points[1], controlPoint2: points[2])
            image = drawLine()
        }else if points.count == 5 {
            points[3] = CGPoint(x: (points[2].x + points[4].x) * 0.5, y: (points[2].y + points[4].y) * 0.5)
            if points[4] != points[3] {
                let length = hypot(points[4].x - points[3].x, points[4].y - points[3].y) / 2.0
                let angle = atan2(points[3].y - points[2].y, points[4].x - points[3].x)
                let controlPoint = CGPoint(x: points[3].x + cos(angle) * length, y: points[3].y + sin(angle) * length)
                temporaryPath = UIBezierPath()
                temporaryPath?.move(to: points[3])
                temporaryPath?.lineWidth = penSize
                temporaryPath?.lineCapStyle = .round
                temporaryPath?.lineJoinStyle = .round
                temporaryPath?.addQuadCurve(to: points[4], controlPoint: controlPoint)
            } else {
                temporaryPath = nil
            }
            path?.move(to: points[0])
            path?.addCurve(to: points[3], controlPoint1: points[1], controlPoint2: points[2])
            points = [points[3], points[4]]
            image = drawLine()
        }
        if pointCount > 50 {
            temporaryPath = nil
            snapshotImage = drawLine()
            path.removeAllPoints()
            pointCount = 0
        }
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        let currentPoint = touches.first!.location(in: self)
        if !isCallTouchMoved { path?.addLine(to: currentPoint) }
        image = drawLine()
        lastDrawImage = image
        temporaryPath = nil
        snapshotImage = nil
        isCallTouchMoved = false
    }
    
    func drawLine() -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
        if snapshotImage != nil {
            snapshotImage?.draw(at: CGPoint.zero)
        }else {
            lastDrawImage?.draw(at: CGPoint.zero)
        }
        penColor.setStroke()
        path?.stroke()
        temporaryPath?.stroke()
        let capturedImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return capturedImage
    }
}
```

# まとめと感想
**線を描く機能**はいくつかの問題を乗り越えてなんとか実装できました...
しかし、これよりも描きやすいお絵描きアプリはやっぱりたくさんあって
アプリの差別化を図るのは難しい事だなと感じました。
ただ、もし何らかの線を引いたりするようなアプリの実装について少しでもこの記事が参考になれば嬉しく思います。

また、このカレンダーに参加できてとても良かったです。ありがとうございます。