RippleButtonはボタンをタップしたときに波紋が広がるアニメーションの付いたボタンです。
この記事では、RippleButtonを実装する方法を紹介します。
RippleButton

RippleButtonのコード
UIButtonを継承して以下のように作成しました。
Storyboardやコード内でUIButtonを使用するのと同じように使用できます。
import UIKit
class RippleButton: UIButton {
// ハイライトを無効化
override var isHighlighted: Bool {
didSet { if isHighlighted { isHighlighted = false } }
}
let rippleView = UIView()
let rippleBackgroundView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecorder: NSCoder) {
super.init(coder: aDecorder)
initialize()
}
private func initialize() {
isExclusiveTouch = true // 同時タップを禁止する
setupRipple()
addTarget(self, action: #selector(touchDragEnter(_:forEvent:)), for: .touchDragEnter)
addTarget(self, action: #selector(touchDragExit(_:)), for: .touchDragExit)
}
private func setupRipple() {
rippleBackgroundView.alpha = 0
rippleBackgroundView.clipsToBounds = true
addSubview(rippleBackgroundView)
// AutoLayout
rippleBackgroundView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
rippleBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
rippleBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
rippleBackgroundView.topAnchor.constraint(equalTo: topAnchor),
rippleBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
setupRippleView()
}
private func setupRippleView() {
rippleView.backgroundColor = UIColor(white: 1, alpha: 0.2)
// Rippleが広がったときに、画面全体を覆う大きさを設定
let screenSize = UIScreen.main.bounds
let radius = (pow(screenSize.width, 2) + pow(screenSize.height, 2)).squareRoot()
let diameter = 2 * radius
rippleView.bounds.size = CGSize(width: diameter, height: diameter)
rippleView.layer.cornerRadius = radius
rippleView.clipsToBounds = true
rippleBackgroundView.addSubview(rippleView)
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let location = touch.location(in: self)
animateRipple(location)
return super.beginTracking(touch, with: event)
}
override func cancelTracking(with event: UIEvent?) {
super.cancelTracking(with: event)
animateToNormal()
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
super.endTracking(touch, with: event)
animateToNormal()
}
@objc
private func touchDragEnter(_ sender: UIButton, forEvent event: UIEvent) {
let touches = event.touches(for: sender)
if let location = touches?.first?.location(in: sender) {
animateRipple(location)
}
}
@objc
private func touchDragExit(_ sender: UIButton) {
animateToNormal()
}
private func animateRipple(_ point: CGPoint) {
// Rippleの位置を設定
rippleView.center = point
// タップ直後のripple直径を44に設定
let scale = 44 / max(rippleView.bounds.width, rippleView.bounds.height)
rippleView.transform = CGAffineTransform(scaleX: scale, y: scale)
UIView.animate(
withDuration: 0.1,
delay: 0,
options: .allowUserInteraction,
animations: { self.rippleBackgroundView.alpha = 1 },
completion: nil
)
UIView.animate(
withDuration: 1.0,
delay: 0,
options: [.curveEaseOut, .allowUserInteraction],
animations: { self.rippleView.transform = .identity },
completion: nil
)
}
private func animateToNormal() {
UIView.animate(
withDuration: 0.7,
delay: 0,
options: .allowUserInteraction,
animations: { self.rippleBackgroundView.alpha = 0 },
completion: nil
)
}
}
Rippleの動きに細かな用件がある場合は調整が必要ですが、ライブラリーを使用するよりも自前で作成した方が調整しやすいかと思います。