SwiftUI GeometryEffect
GeometryEffect は、ビューの座標変換をアニメーション可能な形で定義するプロトコルだ。回転、拡大縮小、移動といった幾何学的な変換を、SwiftUI の補間システムを使って滑らかにアニメーションできる。
GeometryEffect の基本構造
GeometryEffect は AnimatableModifier に似ているが、ProjectionTransform を返す点が異なる。
struct SkewEffect: GeometryEffect {
var skewX: CGFloat
var animatableData: CGFloat {
get { skewX }
set { skewX = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
let transform = CGAffineTransform(a: 1, b: 0, c: skewX, d: 1, tx: 0, ty: 0)
return ProjectionTransform(transform)
}
}effectValue で返す ProjectionTransform により、ビューの描画位置・形状が変換される。
CGAffineTransform との関係
ProjectionTransform は CGAffineTransform または CATransform3D から作成できる。
// 2D 変換
ProjectionTransform(CGAffineTransform.identity)
// 3D 変換
ProjectionTransform(CATransform3DIdentity)CGAffineTransform は 2D の行列変換(移動、回転、拡大縮小、せん断)を表現し、CATransform3D は 3D 変換(遠近法を含む)を表現する。
実践例:3D フリップエフェクト
struct FlipEffect: GeometryEffect {
var angle: Double
var axis: (x: CGFloat, y: CGFloat, z: CGFloat)
var animatableData: Double {
get { angle }
set { angle = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
let radians = angle * .pi / 180
var transform3D = CATransform3DIdentity
transform3D.m34 = -1 / 500 // 遠近法
transform3D = CATransform3DRotate(
transform3D,
radians,
axis.x, axis.y, axis.z
)
// 中心を基準に回転
let anchorX = size.width / 2
let anchorY = size.height / 2
let offset = CATransform3DMakeTranslation(anchorX, anchorY, 0)
let offsetBack = CATransform3DMakeTranslation(-anchorX, -anchorY, 0)
let combined = CATransform3DConcat(
CATransform3DConcat(offsetBack, transform3D),
offset
)
return ProjectionTransform(combined)
}
}
extension View {
func flip3D(angle: Double, axis: (CGFloat, CGFloat, CGFloat)) -> some View {
modifier(FlipEffect(angle: angle, axis: axis))
}
}struct FlipDemo: View {
@State private var flipped = false
var body: some View {
RoundedRectangle(cornerRadius: 20)
.fill(.blue)
.frame(width: 200, height: 150)
.flip3D(
angle: flipped ? 180 : 0,
axis: (0, 1, 0)
)
.onTapGesture {
withAnimation(.spring()) {
flipped.toggle()
}
}
}
}タップするとカードが Y 軸を中心に回転する 3D フリップアニメーションになる。
実践例:波打つエフェクト
struct WaveEffect: GeometryEffect {
var progress: Double
var amplitude: CGFloat
var frequency: Double
var animatableData: Double {
get { progress }
set { progress = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
let offset = sin(progress * .pi * 2 * frequency) * amplitude
let transform = CGAffineTransform(translationX: 0, y: offset)
return ProjectionTransform(transform)
}
}progress を 0 から 1 にアニメーションさせると、ビューが上下に波打つ。
ignoredByLayout
GeometryEffect はデフォルトでレイアウトに影響しない。つまり、変換後のビューが他のビューの配置を押しのけることはない。
HStack {
Text("A")
Text("B")
.modifier(SomeGeometryEffect()) // 変換されても A と C の位置は変わらない
Text("C")
}これは ignoredByLayout() がデフォルトで適用されているためだ。レイアウトに影響させたい場合は、カスタム実装が必要になる。
rotation3DEffect との違い
標準の rotation3DEffect も 3D 回転を提供するが、GeometryEffect を使う利点がある。
手軽に使える。anchorZ など追加パラメータあり。しかし、複合変換や独自の数式を適用するのは難しい。
完全にカスタマイズ可能。複数の変換を組み合わせたり、時間に応じた複雑な数式を適用したりできる。
複数の値をアニメーション
AnimatablePair を使って複数のパラメータをアニメーションできる。
struct ComplexEffect: GeometryEffect {
var rotation: Double
var scale: CGFloat
var animatableData: AnimatablePair<Double, CGFloat> {
get { AnimatablePair(rotation, scale) }
set {
rotation = newValue.first
scale = newValue.second
}
}
func effectValue(size: CGSize) -> ProjectionTransform {
let angle = rotation * .pi / 180
let anchorX = size.width / 2
let anchorY = size.height / 2
var transform = CGAffineTransform.identity
transform = transform.translatedBy(x: anchorX, y: anchorY)
transform = transform.rotated(by: angle)
transform = transform.scaledBy(x: scale, y: scale)
transform = transform.translatedBy(x: -anchorX, y: -anchorY)
return ProjectionTransform(transform)
}
}回転とスケールが同時にアニメーションし、複合的な動きを実現できる。
GeometryEffect は低レベルな API だが、標準のモディファイアでは実現できない変換アニメーションを作るための強力なツールだ。3D 的な演出やインタラクティブな UI を作る際に活用できる。