9.3 KiB
9.3 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ios-swiftui-animation-deep | SwiftUI Animation — implicit / explicit / matchedGeometry | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
SwiftUI Animation
SwiftUI 의 animation 가 declarative.
.animation(),withAnimation, transition, matchedGeometry. iOS 17+ 의 PhaseAnimator / KeyframeAnimator.
📖 핵심 개념
- Implicit: state 변경 = 자동.
- Explicit:
withAnimationblock. - Transition: appear / disappear.
- matchedGeometry: 두 view 간 morph.
💻 코드 패턴
Implicit animation
struct ContentView: View {
@State var scale = 1.0
var body: some View {
Circle()
.scaleEffect(scale)
.animation(.spring(), value: scale)
.onTapGesture {
scale = scale == 1.0 ? 2.0 : 1.0
}
}
}
→ .animation(_, value:) 가 modern API. Value 변경 = animate.
Explicit (withAnimation)
@State var isExpanded = false
Button("Toggle") {
withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
isExpanded.toggle()
}
}
→ Block 안 변경 가 animate.
모든 animation type
.animation(.linear(duration: 0.3), value: x)
.animation(.easeInOut(duration: 0.5), value: x)
.animation(.spring(response: 0.5, dampingFraction: 0.7), value: x)
.animation(.bouncy, value: x) // iOS 17
.animation(.smooth, value: x) // iOS 17
.animation(.snappy, value: x) // iOS 17
.animation(.interactiveSpring(), value: x)
Transition
struct Toast: View {
@State var show = false
var body: some View {
VStack {
if show {
Text("Hello")
.transition(.move(edge: .top).combined(with: .opacity))
}
}
.animation(.spring(), value: show)
}
}
→ Appear / disappear 의 animation.
Built-in transitions
.transition(.opacity)
.transition(.scale)
.transition(.slide)
.transition(.move(edge: .leading))
.transition(.push(from: .top)) // iOS 17
.transition(.scale.combined(with: .opacity))
Custom transition (iOS 17 movePhase)
struct BlurTransition: Transition {
func body(content: Content, phase: TransitionPhase) -> some View {
content
.blur(radius: phase.isIdentity ? 0 : 20)
.opacity(phase.isIdentity ? 1 : 0)
}
}
// 사용
.transition(BlurTransition())
matchedGeometryEffect (morph)
@Namespace var namespace
@State var expanded = false
VStack {
if expanded {
Image("hero")
.matchedGeometryEffect(id: "hero", in: namespace)
.frame(width: 300, height: 300)
} else {
Image("hero")
.matchedGeometryEffect(id: "hero", in: namespace)
.frame(width: 100, height: 100)
}
}
.onTapGesture {
withAnimation(.spring()) { expanded.toggle() }
}
→ 같은 ID = morph. Hero animation 의 답.
PhaseAnimator (iOS 17+)
struct PulseView: View {
var body: some View {
Circle()
.phaseAnimator([1.0, 1.5, 1.0]) { content, phase in
content
.scaleEffect(phase)
.opacity(phase == 1.5 ? 0.5 : 1.0)
} animation: { phase in
.easeInOut(duration: 0.5)
}
}
}
→ 자동 cycle. Loop 식 animation.
KeyframeAnimator (iOS 17+)
struct WaveView: View {
@State var trigger = false
var body: some View {
Circle()
.keyframeAnimator(initialValue: AnimationValues(), trigger: trigger) { content, value in
content
.scaleEffect(value.scale)
.rotationEffect(value.rotation)
} keyframes: { _ in
KeyframeTrack(\.scale) {
LinearKeyframe(1.5, duration: 0.5)
SpringKeyframe(1.0, duration: 0.5)
}
KeyframeTrack(\.rotation) {
CubicKeyframe(.degrees(360), duration: 1.0)
}
}
.onTapGesture { trigger.toggle() }
}
}
struct AnimationValues {
var scale = 1.0
var rotation = Angle.zero
}
→ 복잡 multi-property animation.
Animatable protocol (custom)
struct CounterShape: Shape, Animatable {
var value: Double
var animatableData: Double {
get { value }
set { value = newValue }
}
func path(in rect: CGRect) -> Path {
// value 가 변경 시 매 frame draw
}
}
→ Custom shape 도 animate.
AnimatableModifier
struct ShakeEffect: GeometryEffect {
var amount: CGFloat = 10
var animatableData: CGFloat
func effectValue(size: CGSize) -> ProjectionTransform {
ProjectionTransform(CGAffineTransform(translationX:
amount * sin(animatableData * .pi * 5), y: 0))
}
}
// 사용
.modifier(ShakeEffect(animatableData: CGFloat(attempts)))
Spring (커스텀)
.animation(.spring(
response: 0.5, // duration approximation
dampingFraction: 0.7, // overshoot (0 = bounce, 1 = no bounce)
blendDuration: 0
), value: x)
→ response + dampingFraction 가 일반.
Animation interruption
// 진행 중 animation 가 다른 변경 시:
withAnimation(.easeInOut) { x = 100 }
// 0.2 sec 후
withAnimation(.spring()) { x = 50 }
// → 2번째 가 1번째 의 현재 값 부터 시작 (smooth).
→ SwiftUI 가 자동.
Animation curve
extension Animation {
static var customCurve: Animation {
.timingCurve(0.2, 0.8, 0.2, 1.0, duration: 0.5)
}
}
→ .timingCurve(c1x, c1y, c2x, c2y) = bezier.
Disable animation (specific)
.transaction { tx in
tx.animation = nil
}
// Or
withTransaction(Transaction(animation: nil)) {
x = 100
}
→ 일부 변경 가 animate X.
List / scroll animation
List(items, id: \.id) { item in
Row(item)
}
.animation(.spring(), value: items)
// onDelete + animation
.swipeActions {
Button("Delete") {
withAnimation { items.removeAll { $0.id == item.id } }
}
}
Scroll 의 visual effect (iOS 17)
ScrollView {
ForEach(items) { item in
Card(item)
.scrollTransition { content, phase in
content
.opacity(phase.isIdentity ? 1 : 0.3)
.scaleEffect(phase.isIdentity ? 1 : 0.8)
}
}
}
→ Scroll 위치 따라 modify.
Repeat animation
.animation(.linear(duration: 1).repeatForever(autoreverses: true), value: x)
// Or
withAnimation(.linear(duration: 1).repeatForever()) {
rotation = 360
}
Performance
- 매 frame draw 안 비싸게.
- Complex shape = lazy.
- Background work + animation 가 main thread X.
- Instruments > Animation Hitches.
Geometry effect (transform)
struct RotationEffect: GeometryEffect {
var angle: Double
var animatableData: Double {
get { angle }
set { angle = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
ProjectionTransform(CGAffineTransform(rotationAngle: angle))
}
}
함정
- @State 변경 + animation 없음: 즉시.
- ObservableObject 변경: 자동 X (withAnimation 명시).
- async work 후 변경: main thread + withAnimation.
- 큰 list animation: 성능.
- transition 없이 conditional: 즉시 fade.
Reduce motion (a11y)
@Environment(\.accessibilityReduceMotion) var reduce
withAnimation(reduce ? nil : .spring()) {
x.toggle()
}
→ User 가 motion ↓ 옵션 — 존중.
iOS 17 추가 (modern)
- ContainerRelativeShape
- Shader Library (Metal 통합)
- Symbol effects
- Gradient animatable
→ SwiftUI 매년 발전.
Symbol effect
Image(systemName: "heart.fill")
.symbolEffect(.bounce, value: trigger)
.symbolEffect(.pulse)
.symbolEffect(.variableColor.iterative)
→ SF Symbol 가 자체 animate.
🤔 의사결정 기준
| 작업 | 추천 |
|---|---|
| State 변경 | Implicit .animation(_, value:) |
| 명시 control | withAnimation |
| Appear/disappear | .transition() |
| Hero / morph | matchedGeometryEffect |
| Loop | phaseAnimator |
| 복잡 timeline | keyframeAnimator |
| Custom geometry | Animatable protocol |
| Symbol | symbolEffect |
❌ 안티패턴
- animation(:value:) 없는 implicit: 모든 거 animate.
- withAnimation 안 의 async work: race.
- Complex shape + 매 frame redraw: 성능.
- Transition 없이 conditional: 갑작 사라짐.
- Reduce motion 무시: a11y 위반.
- Spring response 가 너무 작음: jittery.
- Stack 의 모든 child 가 animate: 폭발.
🤖 LLM 활용 힌트
.animation(_, value:)가 modern (iOS 16+).- matchedGeometryEffect 가 Hero 답.
- iOS 17 의 phaseAnimator / keyframe 가 powerful.
- Reduce motion 항상 존중.