【Swift4】UIBezierPathをアニメーションさせる
前回、矢印をUIView上に描画する例を示した。
tsukader.hatenablog.com
上の記事では、UIViewのdraw(_ rect: CGRect)関数でUIBezierPathの矢印を描画し、そのViewをMainのViewにaddSubViewするという手法だった。
今回はその矢印をアニメーションによって動かしてみようと思う。
方法としては、CAShapeLayer上にUIBezierPathで矢印を描画し、CoreAnimationのCABasicAnimationによってその矢印を動かす。
CAShapeLayerはUIViewのサブクラスとして定義したRepeatArrowクラスのlayerにaddSublayerする。
まずは前回同様、UIBezierPathをextensionによって拡張し、矢印を描画する関数「arrow」を定義する。
⌘Nで新しいSwiftファイルを生成し、以下のコードを記述する。このファイル名は任意。
extension UIBezierPath { static func arrow(from start: CGPoint, to end: CGPoint, tailWidth: CGFloat, headWidth: CGFloat, headLength: CGFloat) -> UIBezierPath { let length = hypot(end.x - start.x, end.y - start.y) let tailLength = length - headLength func p(_ x: CGFloat, _ y: CGFloat) -> CGPoint { return CGPoint(x: x, y: y) } let points: [CGPoint] = [ p(0, tailWidth / 2), p(tailLength, tailWidth / 2), p(tailLength, headWidth / 2), p(length, 0), p(tailLength, -headWidth / 2), p(tailLength, -tailWidth / 2), p(0, -tailWidth / 2) ] let cosine = (end.x - start.x) / length let sine = (end.y - start.y) / length let transform = CGAffineTransform(a: cosine, b: sine, c: -sine, d: cosine, tx: start.x, ty: start.y) let path = CGMutablePath() path.addLines(between: points, transform: transform) path.closeSubpath() return self.init(cgPath: path) } }
これでいつでも矢印を描画できる。
次に、矢印を描画するためのUIViewと、その中のLayerを定義する。クラス名はRepeatArrowとする。
同じく⌘Nで新しいSwiftファイルを作成し、以下のコードを記述する。ファイル名はRepeatArrow.swiftが良いだろう。
class RepeatArrow: UIView { var arrow: CAShapeLayer? var fromPoint: CGPoint! var startArrowToPoint: CGPoint! var toPoint: CGPoint! var startArrow: UIBezierPath! var endArrow: UIBezierPath! init(frame: CGRect, from: CGPoint, to: CGPoint) { super.init(frame: frame) fromPoint = from toPoint = to startArrowToPoint = CGPoint(x: from.x + (to.x-from.x)*0.01, y: from.y + (to.y-from.y)*0.01) self.isOpaque = false self.isUserInteractionEnabled = false self.createArrow() self.animateArrow() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func animateArrow() { let bounce = CABasicAnimation(keyPath: "path") bounce.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn) //startArrow & endArrow startArrow = UIBezierPath.arrow(from: fromPoint, to: startArrowToPoint, tailWidth: 0.5, headWidth: 8, headLength: 8) endArrow = UIBezierPath.arrow(from: fromPoint, to: toPoint, tailWidth: 0.5, headWidth: 8, headLength: 8) //bounce bounce.fromValue = self.startArrow.cgPath bounce.toValue = self.endArrow.cgPath bounce.duration = 3.0 bounce.isRemovedOnCompletion = false bounce.fillMode = kCAFillModeForwards bounce.repeatCount = .infinity self.arrow?.add(bounce, forKey: "return") } func createArrow(){ arrow = CAShapeLayer(layer: self.layer) arrow?.lineWidth = 0.9 arrow?.path = UIBezierPath.arrow(from: fromPoint, to: startArrowToPoint, tailWidth: 0.5, headWidth: 8, headLength: 8).cgPath arrow?.strokeColor = UIColor.black.cgColor arrow?.fillColor = UIColor.black.cgColor self.layer.addSublayer(arrow!) } }
手順としては、createArrow関数によって初期状態の矢印を生成し、それをanimateArrow関数によって初期状態から最終状態までアニメーションさせている。
あとは矢印を描画したいView上で以下のようにして矢印を生成する。
let arrow1 = RepeatArrow(frame: self.view.frame, from: CGPoint(x:100, y:100), to: CGPoint(x:200, y:200)) self.view.addSubview(arrow1)
これで、好きな位置に矢印を描画することができますね。
さいごに
今回は始点から終点まで伸びる矢印を作りたかったため、初期状態では.arrow(from: 始点, to: 始点)というような矢印であって欲しかったが、それでは始点と終点が同じ位置になるせいでエラーが返されてしまったので、startArrowToPointというCGPointを用意し、fromPointからtoPoint方向に全体の0.01倍だけ進んだ点を取り、初期状態の終点とした。
Error: this application, or a library it uses, has passed an invalid numeric value (NaN, or not-a-number) to CoreGraphics API and this value is being ignored. Please fix this problem.
といったエラーが出たら、そのような指定をしていないか疑ってみると良い。
また、Core Animationについては以下の資料が参考になるだろう。
アニメーション終了時に減速させる設定や、アニメーションの繰り返しなどについて詳しく解説されている。
https://developer.apple.com/jp/documentation/CoreAnimation_guide.pdf
https://developer.apple.com/jp/documentation/Animation_Types_Timing.pdf