반응형
SwiftUI PhaseAnimator와 KeyframeAnimator를 활용한 고급 애니메이션 구현
SwiftUI에서 기존의 withAnimation이나 단순 .animation() 모디파이어는 정적 상태 간 전환에는 유용하지만, 다단계 연속 동작이나 속성별 독립적 타이밍을 제어하는 데는 한계가 존재합니다. iOS 17 이상부터 제공되는 PhaseAnimator와 KeyframeAnimator는 복잡한 애니메이션을 선언적이고 직관적인 코드로 구축할 수 있는 API를 제공합니다.
1. PhaseAnimator: 단계별 시퀀스 애니메이션
PhaseAnimator는 정해진 단계(Phase)를 순차적으로 순환하며 뷰의 상태와 애니메이션 곡선을 변경하는 방식입니다. @State 변수를 여러 개 생성하여 중첩 관리할 필요 없이, 열거형(Enum)이나 배열로 단계를 정의하여 처리합니다.
열거형 기반 단계 정의 및 트리거 연동
상태 연출 화면과 하단 실행 컨트롤 버튼을 분리한 구현 예제입니다.
import SwiftUI
// 1. 애니메이션 단계 정의
enum HeartPhase: CaseIterable {
case initial
case enlarged
case compressed
var scale: CGFloat {
switch self {
case .initial: return 1.0
case .enlarged: return 1.4
case .compressed: return 0.95
}
}
var color: Color {
switch self {
case .initial: return .gray
case .enlarged: return .pink
case .compressed: return .red
}
}
}
struct PhaseAnimatorTestView: View {
@State private var trigger = false
var body: some View {
VStack(spacing: 24) {
// UI 화면 영역 (Stage)
VStack {
Spacer()
Image(systemName: trigger ? "heart.fill" : "heart")
.resizable()
.scaledToFit()
.frame(width: 80, height: 80)
.phaseAnimator(
HeartPhase.allCases,
trigger: trigger
) { content, phase in
content
.scaleEffect(trigger ? phase.scale : 1.0)
.foregroundStyle(trigger ? phase.color : .gray)
} animation: { phase in
switch phase {
case .initial: .easeOut(duration: 0.1)
case .enlarged: .spring(response: 0.3, dampingFraction: 0.5)
case .compressed: .easeInOut(duration: 0.15)
}
}
Spacer()
}
.frame(maxWidth: .infinity, minHeight: 220)
.background(Color(.secondarySystemBackground))
.cornerRadius(16)
// 하단 별도 테스트 실행 버튼
Button(action: {
trigger.toggle()
}) {
Label("PhaseAnimator 트리거 실행", systemImage: "play.fill")
.font(.headline)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
}
.buttonStyle(.borderedProminent)
.tint(.pink)
}
.padding()
}
}
- trigger: 하단 버튼 클릭 시 값이 동적으로 변경되며 정의된 단계를 1회 순환합니다.
- animation: 각 phase로 전환될 때 서로 다른 트랜지션 곡선(spring, easeOut 등)을 다르게 지정합니다.
Phaseanimator 사용예

2. KeyframeAnimator: 독립적 타임라인 기반 키프레임 제어
KeyframeAnimator는 뷰의 여러 속성(크기, 회전, 위치, 불투명도 등)을 개별 타임라인 트랙(KeyframeTrack)으로 분리하여 고유한 시점과 인터폴레이션 방식으로 정밀하게 제어할 때 사용됩니다.
복합 키프레임 애니메이션 구현
연출 화면과 하단 실행 컨트롤 버튼을 분리한 구현 예제입니다.
import SwiftUI
// 1. 애니메이션 대상 속성 집합 정의
struct BellAnimationValues {
var scale: CGFloat = 1.0
var verticalOffset: CGFloat = 0.0
var rotation: Angle = .zero
}
struct KeyframeAnimatorTestView: View {
@State private var triggerCount = 0
var body: some View {
VStack(spacing: 24) {
// UI 화면 영역 (Stage)
VStack {
Spacer()
KeyframeAnimator(
initialValue: BellAnimationValues(),
trigger: triggerCount
) { values in
Image(systemName: triggerCount % 2 == 1 ? "bell.badge.fill" : "bell.fill")
.resizable()
.scaledToFit()
.frame(width: 80, height: 80)
.foregroundStyle(triggerCount % 2 == 1 ? .orange : .gray)
.offset(y: values.verticalOffset)
.rotationEffect(values.rotation)
.scaleEffect(values.scale)
} keyframes: { _ in
KeyframeTrack(\.verticalOffset) {
CubicKeyframe(-18, duration: 0.15)
CubicKeyframe(0, duration: 0.15)
}
KeyframeTrack(\.rotation) {
CubicKeyframe(.degrees(-20), duration: 0.1)
CubicKeyframe(.degrees(20), duration: 0.1)
CubicKeyframe(.degrees(-10), duration: 0.1)
CubicKeyframe(.degrees(0), duration: 0.1)
}
KeyframeTrack(\.scale) {
SpringKeyframe(1.3, duration: 0.2, spring: .bouncy)
SpringKeyframe(1.0, duration: 0.2)
}
}
Spacer()
}
.frame(maxWidth: .infinity, minHeight: 220)
.background(Color(.secondarySystemBackground))
.cornerRadius(16)
// 하단 별도 테스트 실행 버튼
Button(action: {
triggerCount += 1
}) {
Label("KeyframeAnimator 트리거 실행", systemImage: "play.fill")
.font(.headline)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
}
.buttonStyle(.borderedProminent)
.tint(.orange)
}
.padding()
}
}
KeyframeAnimator 사용예

키프레임 보간 종류
| 키프레임 유형 | 보간 특성 |
| LinearKeyframe | 지정된 시간 동안 일정한 속도로 선형 보간 진행 |
| CubicKeyframe | 3차 큐빅 베지어 곡선을 이용해 부드러운 시작과 종료 처리 |
| SpringKeyframe | 스프링 물리 엔진(반발력, 감쇄비) 기반 동적 보간 적용 |
| MoveKeyframe | 보간 없이 특정 시점에 값을 즉시 변경 |
3. PhaseAnimator vs KeyframeAnimator 비교
| 비교 항목 | PhaseAnimator | KeyframeAnimator |
| 제어 단위 | 순차적 단계(Phase) 흐름 | 시간 축 기반 속성별 트랙(Track) |
| 속성 독립성 | 단계별로 전체 뷰 상태 일괄 변경 | 각 프로퍼티별(스케일, 회전 등) 독립 타이밍 적용 |
| 구현 복잡도 | 상대적으로 낮음 (Enum/배열 활용) | 상대적으로 높음 (Value 구조체 및 Track 정의) |
| 적합한 유즈케이스 | 하트 버튼, 탭 바 아이콘 토글 등 | 알림 종 흔들림, 물리적 바운스, 복합 마이크로 인터랙션 |
결론
PhaseAnimator는 단계 중심의 상태 전환 애니메이션 구축에 적합하며, KeyframeAnimator는 시간축 기반의 다중 속성 고정밀 애니메이션 구현 시 뷰의 가독성과 코드 유지보수성을 극대화합니다.
반응형


