現在開発中のアプリで矩形以外の複雑な形をした画像をのタッチイベントを取得する必要があり日本語、英語で軽く検索してみたが、”これだ!”という文献が探せなかったので共有しておきます。Playgroud環境下ですぐにお試しできます。
下準備 〜画像作成〜
最終的にCAShapeLayerのPath(CGPath)に代入するので、Illustrator, Sketch, Figmaなどでベクター画像を作り、PaintCodeなどでUIBezierPathに変換します。
またPocketSVGというライブラリを使えば、SVGファイルをCGPathとして取得できます。
今回はその生成物(UIBezierPath)を解りやすいようにコード内にハードコーディングしていますが、図形の形状が複雑になると長くなってコードの可読性が悪くなるので別ファイル化した方が良いでしょう。
下記はUIBezierPath(上記の生成物をコピー)をCGPathで返す単純なfunctionです。
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のプロパティでパスの線の形状、色、パス内の色などを変更することが可能で、その多くがアニメーション可能です。そのため、簡単にタップしたらアニメーション付きで、色が変わる、大きさが変わるなどの仕掛けを作ることができます。
CALayerはCAShapeLayerの他にも、CATextLayer、CAGradientLayer、CATiledLayer など数々の表示に関するサブクラスがあります。下記記事は英語ですが、それらがよくまとまっているので、一読をお勧めします。
CALayer Tutorial for iOS: Getting Started
#ベクター画像の表示
CAShapeLayerを使った画像の表示手順は、
- CAShapeLayerのインスタンスを生成
- そのインスタンスのpathプロパティに上記で作成したパス(CGPath)を代入。
shapeLayer.path = getPath()
- インスタンスのプロパティで線種、線色、色などを設定(任意)
-
UIViewのlayerにaddSublayerメソッドを使って、上記のCAShapeLayerのインスタンスを追加
self.layer.addSublayer(shapeLayer)
今回は上記手順をfunction(setupView()
)にまとめ、UIViewのカスタムクラスのイニシャライザでコールしています。
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を返します。
この例では画像をタッチした場合は画像を赤に、外であれば画像を白に変更しています。
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にコピー、実行すれば試せます。
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