※ すべてのコードを載せると量が多くなってしまうので、ポイントを絞って説明します。
※ すべて見たい方はGitHubを参照してください。
ポイントを簡潔にまとめると以下のようになります。
- 遷移の始まりと終わりの円のCGPathを取得する
- 遷移先のUIViewControllerのview.layer.maskにCAShapeLayerを代入する
- 取得したCGPathを使ってアニメーションを実行する
画面遷移のアニメーションの実装には、UIViewControllerAnimatedTransitioningを使っています。
遷移の始まりと終わりの円のCGPathを取得する
// 画面全体を覆う円の半径
let radius = { () -> CGFloat in
// centerはタップしたCGPoint
let x = max(self.center.x, containerView.frame.width - self.center.x)
let y = max(self.center.y, containerView.frame.height - self.center.y)
return sqrt(x * x + y * y)
}()
let rectAroundCircle = { (radius: CGFloat) -> CGRect in
return CGRectInset(CGRect(origin: self.center, size: CGSizeZero), -radius, -radius)
}
// アニメーションにおけるはじまりの円と終わりの円のパスを取得
let startPath = CGPathCreateWithEllipseInRect(rectAroundCircle(0), nil)
let endPath = CGPathCreateWithEllipseInRect(rectAroundCircle(radius), nil)
radius
では、タップした点から一番遠い角までの距離が半径が求められます。
つまり、これによって求められるものは、アニメーション終了時の円の半径となります。
rectAroundCircle
は、タップした点から、指定したradius分余白を縦横両方に加えた矩形(CGRect)を取得できます。
これは、次に出てくるCGPathCreateWithEllipseInRect
で、円のCGPathを求めるためのCGRectということになります。
CGPathCreateWithEllipseInRect
は、指定されたCGRectの各辺に接するような楕円のCGPathを得ることができます(参考: CGPathCreateWithEllipseInRectを使って円形のviewを作る)。
先ほどのrectAroundCircle
を使って、アニメーションにおけるはじまりの円と終わりの円のCGPathを求めています。
遷移先のUIViewControllerのview.layer.maskにCAShapeLayerを代入する
// targetは、遷移先のUIViewControllerのview
target.layer.mask = { () -> CALayer in
let mask = CAShapeLayer()
mask.path = endPath
return mask
}()
target.layer.maskにpathを設定できるようにするため、CAShapeLayer(CALayerの子クラス)を代入します。
pathに先ほどの円のCGPathを設定することでアニメーションを実現するからです。
target.layer.maskにpathを設定できるようすればいいだけなので、上のようにendPathを代入しておくことはしなくてもいいのではないかと思うかもしれませんが、設定しておかないとアニメーション終了後にviewが真っ暗になる(pathの値がnilになり、viewに描画されない状態になると思われる)ので、アニメーション前に終了時のpathを設定しておきます。
追記
naoyashigaさんより、上の内容についてご指摘いただきました。
CABasicAnimationのremovedOnCompletion
プロパティをfalseにし、fillMode
プロパティをkCAFillModeForwardsに設定すれば、わざわざあらかじめmask.pathにendPathを指定しておかなくてもアニメーション後の状態を維持することができそうです。
つまり、CABasicAnimationの各プロパティを設定したら、上のコードは単純に以下のようにするだけで大丈夫です。
target.layer.mask = CAShapeLayer()
GitHubも更新してあるので、興味がある方は是非ご覧ください。
取得したCGPathを使ってアニメーションを実行する
// CABasicAnimationは通常このようなイニシャライザは持っていませんので、
// extensionで自作しています。
// duration, timingFunction, delegateは自分で設定。
let animation = CABasicAnimation(keyPath: "path", fromValue: startPath, toValue: endPath, duration: duration, timingFunction: timingFunction, delegate: delegate)
target.layer.mask.addAnimation(animation, forKey: "circular")
CABasicAnimation
のキー値を"path"として、上で求めたpathをそれぞれfromValue, toValueに設定しています。
そして、target.layer.maskに対してこのアニメーションを実装します。
デモ
実際に作ったものが以下のようになります。
このように、GitHubにあげたものには、今回とは逆の、円が小さくなって遷移するようなアニメーションも実装しています。