SwiftUI AnimatableModifier
AnimatableModifier は、カスタムモディファイアにアニメーション機能を追加するためのプロトコルだ。ViewModifier と Animatable を組み合わせることで、SwiftUI の補間システムを利用した独自のアニメーション効果を作成できる。
Animatable プロトコルの基本
Animatable プロトコルは animatableData プロパティを要求する。これにより、SwiftUI がその値を時間に沿って補間できるようになる。
struct ProgressModifier: ViewModifier, Animatable {
var progress: Double
var animatableData: Double {
get { progress }
set { progress = newValue }
}
func body(content: Content) -> some View {
content
.clipShape(
Rectangle()
.size(width: progress * 100, height: 100)
)
}
}progress が 0 から 1 に変化するとき、SwiftUI は中間値(0.1, 0.2, 0.3…)を自動的に計算してアニメーションを生成する。
AnimatableModifier の実装
AnimatableModifier は ViewModifier と Animatable を組み合わせた typealias だ。
// これらは同等
struct MyModifier: ViewModifier, Animatable { ... }
struct MyModifier: AnimatableModifier { ... } // 省略形実践例:パーセント表示アニメーション
数値がカウントアップするアニメーションを作ってみる。
struct CountingModifier: AnimatableModifier {
var number: Double
var animatableData: Double {
get { number }
set { number = newValue }
}
func body(content: Content) -> some View {
Text("\(Int(number))%")
.font(.largeTitle.monospacedDigit())
}
}
extension View {
func countingNumber(_ number: Double) -> some View {
modifier(CountingModifier(number: number))
}
}
struct CountingDemo: View {
@State private var progress: Double = 0
var body: some View {
VStack {
Color.clear
.countingNumber(progress)
Button("Animate") {
withAnimation(.easeInOut(duration: 2)) {
progress = 100
}
}
}
}
}ボタンを押すと、0 から 100 まで数字がアニメーションしながら変化する。
AnimatablePair で複数の値をアニメーション
複数の値を同時にアニメーションさせたい場合は AnimatablePair を使う。
struct WaveModifier: AnimatableModifier {
var amplitude: Double
var frequency: Double
var animatableData: AnimatablePair<Double, Double> {
get { AnimatablePair(amplitude, frequency) }
set {
amplitude = newValue.first
frequency = newValue.second
}
}
func body(content: Content) -> some View {
content
.offset(y: sin(frequency * .pi * 2) * amplitude)
}
}AnimatablePair をネストすれば、3 つ以上の値も扱える。
// 3つの値
var animatableData: AnimatablePair<Double, AnimatablePair<Double, Double>> {
get {
AnimatablePair(first, AnimatablePair(second, third))
}
set {
first = newValue.first
second = newValue.second.first
third = newValue.second.second
}
}実践例:グラデーション位置のアニメーション
struct GradientPositionModifier: AnimatableModifier {
var position: Double
var animatableData: Double {
get { position }
set { position = newValue }
}
func body(content: Content) -> some View {
content
.overlay(
LinearGradient(
colors: [.clear, .white.opacity(0.5), .clear],
startPoint: UnitPoint(x: position - 0.3, y: 0),
endPoint: UnitPoint(x: position + 0.3, y: 1)
)
)
.mask(content)
}
}
extension View {
func shimmer(position: Double) -> some View {
modifier(GradientPositionModifier(position: position))
}
}これを使えば、ローディング時のシマー(光沢が流れる)エフェクトが実装できる。
Shape でのアニメーション
Shape も Animatable に準拠しているため、パスのアニメーションが可能だ。
struct AnimatableArc: Shape {
var endAngle: Double
var animatableData: Double {
get { endAngle }
set { endAngle = newValue }
}
func path(in rect: CGRect) -> Path {
Path { path in
path.addArc(
center: CGPoint(x: rect.midX, y: rect.midY),
radius: rect.width / 2,
startAngle: .degrees(0),
endAngle: .degrees(endAngle),
clockwise: false
)
}
}
}
struct ArcDemo: View {
@State private var progress: Double = 0
var body: some View {
AnimatableArc(endAngle: progress * 360)
.stroke(lineWidth: 10)
.frame(width: 100, height: 100)
.onTapGesture {
withAnimation(.easeInOut(duration: 1)) {
progress = progress == 0 ? 1 : 0
}
}
}
}円弧が滑らかに伸び縮みする。プログレスインジケーターなどに活用できる。
注意点
animatableData の getter/setter はアニメーション中に大量に呼ばれる。重い処理を入れないこと。
animatableData は VectorArithmetic に準拠した型でなければならない。Double、CGFloat、AnimatablePair などが使える。
アニメーション中は body が毎フレーム再評価される。ビュー構造を軽量に保つことが重要。
AnimatableModifier を使いこなすと、SwiftUI の標準アニメーションでは表現できない独自の動きを実現できる。ただし、多くの場合は標準のアニメーションで十分なので、本当に必要な場面で活用するのがベストだ。