Animations bring your SwiftUI apps to life. Learn advanced animation techniques to create engaging user experiences.
Animation Basics
Implicit Animation
swift
struct BasicAnimation: View {
@State private var scale: CGFloat = 1.0
var body: some View {
Circle()
.fill(Color.blue)
.frame(width: 100, height: 100)
.scaleEffect(scale)
.animation(.easeInOut(duration: 0.3), value: scale)
.onTapGesture {
scale = scale == 1.0 ? 1.5 : 1.0
}
}
}Explicit Animation
swift
struct ExplicitAnimation: View {
@State private var offset = CGSize.zero
var body: some View {
Circle()
.fill(Color.orange)
.frame(width: 100, height: 100)
.offset(offset)
.onTapGesture {
withAnimation(.spring()) {
offset = offset == .zero ? CGSize(width: 100, height: 0) : .zero
}
}
}
}Spring Animations
Custom Springs
swift
struct SpringAnimations: View {
@State private var isAnimating = false
var body: some View {
VStack(spacing: 30) {
// Smooth spring
Circle()
.fill(Color.blue)
.frame(width: 60, height: 60)
.scaleEffect(isAnimating ? 1.5 : 1.0)
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: isAnimating)
// Bouncy spring
Circle()
.fill(Color.green)
.frame(width: 60, height: 60)
.scaleEffect(isAnimating ? 1.5 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.4), value: isAnimating)
// Snappy spring
Circle()
.fill(Color.orange)
.frame(width: 60, height: 60)
.scaleEffect(isAnimating ? 1.5 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.6), value: isAnimating)
}
.onTapGesture {
isAnimating.toggle()
}
}
}Keyframe Animations
swift
struct KeyframeAnimation: View {
@State private var isAnimating = false
var body: some View {
RoundedRectangle(cornerRadius: isAnimating ? 30 : 10)
.fill(LinearGradient(
colors: [.blue, .purple],
startPoint: .topLeading,
endPoint: .bottomTrailing
))
.frame(width: 150, height: 150)
.rotationEffect(.degrees(isAnimating ? 180 : 0))
.animation(.keyframe(
duration: 1.5,
values: [
KeyframeTrack(rotation: .keyframes(
values: [0, 90, 180, 270, 360],
times: [0, 0.25, 0.5, 0.75, 1.0]
))
]
), value: isAnimating)
.onTapGesture {
isAnimating.toggle()
}
}
}Transitions
Custom Transitions
swift
struct CustomTransitions: View {
@State private var showDetail = false
var body: some View {
VStack {
if showDetail {
RoundedRectangle(cornerRadius: 20)
.fill(Color.blue)
.frame(width: 300, height: 200)
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity),
removal: .opacity
))
}
Button("Toggle") {
withAnimation(.spring()) {
showDetail.toggle()
}
}
}
}
}Slide Transition
swift
struct SlideTransition: View {
@State private var showCard = false
var body: some View {
ZStack {
Color.gray.opacity(0.2)
.ignoresSafeArea()
if showCard {
RoundedRectangle(cornerRadius: 20)
.fill(Color.white)
.frame(width: 300, height: 400)
.shadow(radius: 10)
.transition(.move(edge: .bottom).combined(with: .opacity))
}
}
.onTapGesture {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
showCard.toggle()
}
}
}
}Gesture-Driven Animations
Drag to Dismiss
swift
struct DragToDismiss: View {
@State private var offset: CGFloat = 0
@State private var isDragging = false
var body: some View {
GeometryReader { geometry in
RoundedRectangle(cornerRadius: 20)
.fill(Color.blue)
.frame(width: 300, height: 400)
.offset(y: offset)
.gesture(
DragGesture()
.onChanged { value in
isDragging = true
offset = value.translation.height
}
.onEnded { value in
isDragging = false
withAnimation(.spring()) {
if value.translation.height > 100 {
offset = geometry.size.height
} else {
offset = 0
}
}
}
)
.animation(.spring(response: 0.4, dampingFraction: 0.8), value: isDragging)
}
}
}Swipe Gestures
swift
struct SwipeGestures: View {
@State private var offset: CGFloat = 0
@State private var color: Color = .blue
var body: some View {
RoundedRectangle(cornerRadius: 15)
.fill(color)
.frame(width: 300, height: 80)
.offset(x: offset)
.gesture(
DragGesture()
.onChanged { value in
if value.translation.width > 0 {
offset = value.translation.width
color = .green
} else {
offset = value.translation.width
color = .red
}
}
.onEnded { value in
withAnimation(.spring()) {
if abs(value.translation.width) > 100 {
offset = value.translation.width > 0 ? 200 : -200
} else {
offset = 0
color = .blue
}
}
}
)
.animation(.spring(response: 0.4, dampingFraction: 0.7), value: offset)
}
}Matching Geometry Effect
Hero Animation
swift
struct HeroAnimation: View {
@Namespace private var hero
@State private var isExpanded = false
var body: some View {
VStack {
if isExpanded {
RoundedRectangle(cornerRadius: 20)
.fill(LinearGradient(
colors: [.purple, .pink],
startPoint: .topLeading,
endPoint: .bottomTrailing
))
.frame(width: 300, height: 300)
.matchedGeometryEffect(id: "card", in: hero)
} else {
RoundedRectangle(cornerRadius: 10)
.fill(LinearGradient(
colors: [.purple, .pink],
startPoint: .topLeading,
endPoint: .bottomTrailing
))
.frame(width: 150, height: 150)
.matchedGeometryEffect(id: "card", in: hero)
}
}
.onTapGesture {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
isExpanded.toggle()
}
}
}
}Sequential Animations
Animation Sequence
swift
struct SequentialAnimations: View {
@State private var scale1: CGFloat = 1.0
@State private var scale2: CGFloat = 1.0
@State private var scale3: CGFloat = 1.0
var body: some View {
HStack(spacing: 20) {
Circle()
.fill(Color.blue)
.frame(width: 60, height: 60)
.scaleEffect(scale1)
Circle()
.fill(Color.green)
.frame(width: 60, height: 60)
.scaleEffect(scale2)
Circle()
.fill(Color.orange)
.frame(width: 60, height: 60)
.scaleEffect(scale3)
}
.onTapGesture {
animateSequentially()
}
}
func animateSequentially() {
scale1 = 1.0
scale2 = 1.0
scale3 = 1.0
withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
scale1 = 1.5
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
scale2 = 1.5
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
scale3 = 1.5
}
}
}
}Timeline View
Countdown Animation
swift
struct CountdownAnimation: View {
var body: some View {
TimelineView(.animation(minimumInterval: 1.0)) { context in
let seconds = context.date.timeIntervalSince1970
let remaining = 10 - Int(seconds) % 10
ZStack {
Circle()
.stroke(Color.gray.opacity(0.2), lineWidth: 20)
.frame(width: 200, height: 200)
Circle()
.trim(from: 0, to: CGFloat(remaining) / 10.0)
.stroke(
AngularGradient(
gradient: Gradient(colors: [.blue, .purple]),
center: .center
),
style: StrokeStyle(lineWidth: 20, lineCap: .round)
)
.frame(width: 200, height: 200)
.rotationEffect(.degrees(-90))
.animation(.linear, value: remaining)
Text("\(remaining)")
.font(.system(size: 72, weight: .bold))
}
}
}
}Best Practices
- 1Springs: Use spring animations for natural feel
- 2Performance: Animate view properties, not view hierarchy
- 3Consistency: Use consistent animation timing
- 4Feedback: Provide visual feedback for interactions
- 5Accessibility: Respect reduced motion preferences
- 6Testing: Test animations on different devices
- 7Optimization: Use withAnimation sparingly
- 8UX: Animations should enhance, not distract
Advanced animations create delightful user experiences in SwiftUI!