Files
2nd/10_Wiki/Topics/Coding/iOS_SwiftUI_Animation_Deep.md
T
2026-05-10 22:08:15 +09:00

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
ios
swiftui
animation
vibe-coding
language applicable_to
Swift
iOS
SwiftUI animation
withAnimation
matchedGeometryEffect
transition
PhaseAnimator
KeyframeAnimator

SwiftUI Animation

SwiftUI 의 animation 가 declarative. .animation(), withAnimation, transition, matchedGeometry. iOS 17+ 의 PhaseAnimator / KeyframeAnimator.

📖 핵심 개념

  • Implicit: state 변경 = 자동.
  • Explicit: withAnimation block.
  • 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 항상 존중.

🔗 관련 문서