11
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-05-30

現在開発中のアプリで矩形以外の複雑な形をした画像をのタッチイベントを取得する必要があり日本語、英語で軽く検索してみたが、”これだ!”という文献が探せなかったので共有しておきます。Playgroud環境下ですぐにお試しできます。
ezgif-5-9653f8f1f6ea.gif

下準備 〜画像作成〜

最終的にCAShapeLayerのPath(CGPath)に代入するので、Illustrator, Sketch, Figmaなどでベクター画像を作り、PaintCodeなどでUIBezierPathに変換します。
またPocketSVGというライブラリを使えば、SVGファイルCGPathとして取得できます。
Screen Shot 2019-05-30 at 12.15.53 AM.png

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

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は名前からわかるようにCALayerのサブクラスで、UIBezierPathなどで作ったベクター画像を表示します。ベクター画像は通常のイメージと異なり拡大縮小してもボケたりジャギーが出たりすることがないので、@2x@3xの画像を作成する必要がありません。またCAShapeLayerのプロパティでパスの線の形状、色、パス内の色などを変更することが可能で、その多くがアニメーション可能です。そのため、簡単にタップしたらアニメーション付きで、色が変わる、大きさが変わるなどの仕掛けを作ることができます。

CALayerCAShapeLayerの他にも、CATextLayerCAGradientLayerCATiledLayer など数々の表示に関するサブクラスがあります。下記記事は英語ですが、それらがよくまとまっているので、一読をお勧めします。
CALayer Tutorial for iOS: Getting Started

#ベクター画像の表示
CAShapeLayerを使った画像の表示手順は、

  1. CAShapeLayerのインスタンスを生成
  2. そのインスタンスのpathプロパティに上記で作成したパス(CGPath)を代入。
     shapeLayer.path = getPath()
  3. インスタンスのプロパティで線種、線色、色などを設定(任意)
  4. UIViewlayeraddSublayerメソッドを使って、上記のCAShapeLayerのインスタンスを追加
     self.layer.addSublayer(shapeLayer)
    今回は上記手順をfunction(setupView())にまとめ、UIViewのカスタムクラスのイニシャライザでコールしています。
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に画像を表示しているので、先ずはUIViewtouchesBeganをオーバーライドしてUITouchからタッチしたポイントを取得しています。let point = touch!.location(in: self)
その後、CGPathcontainsメソッドでそのポイントがパス内にあるかどうか確認しています。shapeLayer.path!.contains(point) このメソッドは領域内であればtrueを、外ならfalseを返します。
この例では画像をタッチした場合は画像を赤に、外であれば画像を白に変更しています。

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にコピー、実行すれば試せます。

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

11
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?