iPhone、iPadで複雑な形状をした図形内のタッチイベントを簡単に取得する方法　〜CAShapeLayer編〜


現在開発中のアプリで矩形以外の複雑な形をした画像をのタッチイベントを取得する必要があり日本語、英語で軽く検索してみたが、”これだ！”という文献が探せなかったので共有しておきます。Playgroud環境下ですぐにお試しできます。
![ezgif-5-9653f8f1f6ea.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24726/07d2f375-81e3-382f-d611-dda525925e36.gif)

# 下準備　〜画像作成〜
最終的に**CAShapeLayer**のPath(**CGPath**)に代入するので、Illustrator, Sketch, Figmaなどでベクター画像を作り、[PaintCode](https://www.paintcodeapp.com)などで**UIBezierPath**に変換します。
また[PocketSVG](https://github.com/pocketsvg/PocketSVG)というライブラリを使えば、**SVGファイル**を**CGPath**として取得できます。
<img width="1055" alt="Screen Shot 2019-05-30 at 12.15.53 AM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24726/54107caf-c522-3226-5e1f-28b0b4253feb.png">

今回はその生成物（**UIBezierPath**)を解りやすいようにコード内にハードコーディングしていますが、図形の形状が複雑になると長くなってコードの可読性が悪くなるので別ファイル化した方が良いでしょう。
下記は**UIBezierPath**（上記の生成物をコピー）を**CGPath**で返す単純なfunctionです。

```swift:UIBezierPath
    func getPath()  -> CGPath {
        //Start: Created by PaintCode
        let bezierPath = UIBezierPath()
        bezierPath.move(to: CGPoint(x: 13.08, y: 129.49))
        bezierPath.addLine(to: CGPoint(x: 1.58, y: 117.99))
        bezierPath.addLine(to: CGPoint(x: 14.58, y: 104.99))
        bezierPath.addLine(to: CGPoint(x: 0.71, y: 91.11))
        bezierPath.addLine(to: CGPoint(x: 27.71, y: 64.1))
        bezierPath.addLine(to: CGPoint(x: 27.71, y: 53.7))
        bezierPath.addLine(to: CGPoint(x: 49.97, y: 31.45))
        bezierPath.addLine(to: CGPoint(x: 49.97, y: 23.61))
        bezierPath.addLine(to: CGPoint(x: 73.08, y: 0.5))
        bezierPath.addLine(to: CGPoint(x: 101.97, y: 0.5))
        bezierPath.addLine(to: CGPoint(x: 110.6, y: 9.13))
        bezierPath.addLine(to: CGPoint(x: 125.95, y: 9.13))
        bezierPath.addLine(to: CGPoint(x: 155.8, y: 39.71))
        bezierPath.addLine(to: CGPoint(x: 155.8, y: 52.35))
        bezierPath.addLine(to: CGPoint(x: 138.37, y: 69.78))
        bezierPath.addLine(to: CGPoint(x: 97.72, y: 29.13))
        bezierPath.addLine(to: CGPoint(x: 61.51, y: 65.34))
        bezierPath.addLine(to: CGPoint(x: 68.66, y: 72.49))
        bezierPath.addLine(to: CGPoint(x: 33.66, y: 107.49))
        bezierPath.addLine(to: CGPoint(x: 47.91, y: 121.74))
        bezierPath.addLine(to: CGPoint(x: 40.16, y: 129.49))
        bezierPath.addLine(to: CGPoint(x: 13.08, y: 129.49))
        bezierPath.close()
        //END
        return bezierPath.cgPath
    }
```

# CAShapeLayerとは？
[CAShapeLayer](https://developer.apple.com/documentation/quartzcore/cashapelayer)は名前からわかるように**CALayer**のサブクラスで、**UIBezierPath**などで作ったベクター画像を表示します。ベクター画像は通常のイメージと異なり拡大縮小してもボケたりジャギーが出たりすることがないので、@2xや@3xの画像を作成する必要がありません。また**CAShapeLayer**のプロパティでパスの線の形状、色、パス内の色などを変更することが可能で、その多くがアニメーション可能です。そのため、簡単にタップしたらアニメーション付きで、色が変わる、大きさが変わるなどの仕掛けを作ることができます。

**CALayer**は**CAShapeLayer**の他にも、**CATextLayer**、**CAGradientLayer**、**CATiledLayer** など数々の表示に関するサブクラスがあります。下記記事は英語ですが、それらがよくまとまっているので、一読をお勧めします。
[CALayer Tutorial for iOS: Getting Started](https://www.raywenderlich.com/402-calayer-tutorial-for-ios-getting-started)

#ベクター画像の表示
**CAShapeLayer**を使った画像の表示手順は、
1. **CAShapeLayer**のインスタンスを生成
2. そのインスタンスのpathプロパティに上記で作成したパス(**CGPath**）を代入。
　`shapeLayer.path = getPath()`
3. インスタンスのプロパティで線種、線色、色などを設定（任意）
4. **UIView**の**layer**に**addSublayer**メソッドを使って、上記の**CAShapeLayer**のインスタンスを追加
　`self.layer.addSublayer(shapeLayer)`
今回は上記手順をfunction(`setupView()`)にまとめ、**UIView**のカスタムクラスのイニシャライザでコールしています。

```swift:UIBezierPath
    private let shapeLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    private func setupView()    {
        shapeLayer.path = getPath()
        shapeLayer.fillColor = UIColor.white.cgColor
        shapeLayer.lineWidth = 2.0
        shapeLayer.strokeColor = UIColor.blue.cgColor
        self.layer.addSublayer(shapeLayer)
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first
        let point = touch!.location(in: self)
        shapeLayer.fillColor = shapeLayer.path!.contains(point) ? UIColor.red.cgColor : UIColor.white.cgColor
    }
```

#タッチイベントの設定
さて、いよいよ本題のタッチイベントの設定です。今回はカスタム**UIView**に画像を表示しているので、先ずは**UIView**の**touchesBegan**をオーバーライドして**UITouch**からタッチしたポイントを取得しています。`let point = touch!.location(in: self)`
その後、**CGPath**の**contains**メソッドでそのポイントがパス内にあるかどうか確認しています。`shapeLayer.path!.contains(point)` このメソッドは領域内であれば**true**を、外なら**false**を返します。
この例では画像をタッチした場合は画像を赤に、外であれば画像を白に変更しています。

```swift:VectorImageTouchEvent
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first
        let point = touch!.location(in: self)
        shapeLayer.fillColor = shapeLayer.path!.contains(point) ? UIColor.red.cgColor : UIColor.white.cgColor
    }
```

#まとめ
知ってしまえばとても簡単です。**CAShapeLayer**素晴らしい。下記にコード全文を載せておきますので、Playgroudにコピー、実行すれば試せます。

```swift:TouchableVectorImageView
import UIKit
import PlaygroundSupport
let v = UIViewController()
PlaygroundPage.current.liveView = v


class TouchableVectorImageView:UIView   {
    private let shapeLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    private func setupView()    {
        shapeLayer.path = getPath()
        shapeLayer.fillColor = UIColor.white.cgColor
        shapeLayer.lineWidth = 2.0
        shapeLayer.strokeColor = UIColor.blue.cgColor
        self.layer.addSublayer(shapeLayer)
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first
        let point = touch!.location(in: self)
        shapeLayer.fillColor = shapeLayer.path!.contains(point) ? UIColor.red.cgColor : UIColor.white.cgColor
        
    }
}

extension TouchableVectorImageView    {
    
    func getPath()  -> CGPath {
        //Start: Created by PaintCode
        let bezierPath = UIBezierPath()
        bezierPath.move(to: CGPoint(x: 13.08, y: 129.49))
        bezierPath.addLine(to: CGPoint(x: 1.58, y: 117.99))
        bezierPath.addLine(to: CGPoint(x: 14.58, y: 104.99))
        bezierPath.addLine(to: CGPoint(x: 0.71, y: 91.11))
        bezierPath.addLine(to: CGPoint(x: 27.71, y: 64.1))
        bezierPath.addLine(to: CGPoint(x: 27.71, y: 53.7))
        bezierPath.addLine(to: CGPoint(x: 49.97, y: 31.45))
        bezierPath.addLine(to: CGPoint(x: 49.97, y: 23.61))
        bezierPath.addLine(to: CGPoint(x: 73.08, y: 0.5))
        bezierPath.addLine(to: CGPoint(x: 101.97, y: 0.5))
        bezierPath.addLine(to: CGPoint(x: 110.6, y: 9.13))
        bezierPath.addLine(to: CGPoint(x: 125.95, y: 9.13))
        bezierPath.addLine(to: CGPoint(x: 155.8, y: 39.71))
        bezierPath.addLine(to: CGPoint(x: 155.8, y: 52.35))
        bezierPath.addLine(to: CGPoint(x: 138.37, y: 69.78))
        bezierPath.addLine(to: CGPoint(x: 97.72, y: 29.13))
        bezierPath.addLine(to: CGPoint(x: 61.51, y: 65.34))
        bezierPath.addLine(to: CGPoint(x: 68.66, y: 72.49))
        bezierPath.addLine(to: CGPoint(x: 33.66, y: 107.49))
        bezierPath.addLine(to: CGPoint(x: 47.91, y: 121.74))
        bezierPath.addLine(to: CGPoint(x: 40.16, y: 129.49))
        bezierPath.addLine(to: CGPoint(x: 13.08, y: 129.49))
        bezierPath.close()
        //END
        return bezierPath.cgPath
    }
}


v.view.addSubview(TouchableVectorImageView(frame: CGRect(x: 50, y: 50, width: 200, height: 200)))
v.view.backgroundColor = UIColor.gray

```