
Day 20 – Animations in SwiftUI
Add motion and interactivity to your UI using SwiftUI’s built-in animation capabilities for smooth, engaging transitions.
1) Implicit animations (one-liners)
Attach .animation(_, value:) to a view. It animates whenever the given value changes.
struct PulseButton: View {
@State private var big = false
var body: some View {
Button("Tap") { big.toggle() }
.padding()
.background(.blue)
.foregroundStyle(.white)
.clipShape(Capsule())
.scaleEffect(big ? 1.2 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.6), value: big)
}
}
2) Explicit animations (wrap changes)
withAnimation animates only the changes inside the closure.
withAnimation(.easeInOut(duration: 0.25)) {
vm.toggleDone(task)
}
Use explicit animations in your To‑Do app when toggling completion or inserting/deleting rows.
3) Transitions (appear/disappear)
Animate how a view enters or leaves the hierarchy.
struct RevealPanel: View {
@State private var show = false
var body: some View {
VStack(spacing: 16) {
Button(show ? "Hide" : "Show") {
withAnimation(.easeInOut) { show.toggle() }
}
if show {
Text("Hello there 👋")
.padding()
.background(.ultraThickMaterial)
.clipShape(RoundedRectangle(cornerRadius: 16))
.transition(.move(edge: .bottom).combined(with: .opacity))
}
}
.padding()
}
}
Common transitions: .opacity, .scale, .slide, or build your own with AnyTransition.
4) Animating your To‑Do row toggle
Add a little pop when a task is completed.
Image(systemName: task.isDone ? "checkmark.circle.fill" : "circle")
.imageScale(.large)
.symbolRenderingMode(.hierarchical)
.scaleEffect(task.isDone ? 1.2 : 1.0)
.foregroundStyle(task.isDone ? .green : .secondary)
.animation(.spring(response: 0.25, dampingFraction: 0.6), value: task.isDone)
And when toggling:
Button {
withAnimation(.spring(response: 0.25, dampingFraction: 0.7)) {
vm.toggleDone(task)
}
} label { /* icon above */ }
.buttonStyle(.plain)
5) Animate list inserts/deletes
SwiftUI animates list mutations if you change the data inside withAnimation.
withAnimation(.easeInOut) {
vm.add(TaskItem(title: "New animated task"))
}
withAnimation(.easeInOut) {
vm.delete(at: offsets)
}
6) Matched geometry for smooth re‑layout
Great for chip → detail or grid → list effects.
struct ChipsDemo: View {
@Namespace private var ns
@State private var selected: String? = nil
let tags = ["SwiftUI","Networking","Core Data","Testing"]
var body: some View {
VStack(spacing: 20) {
HStack {
ForEach(tags, id: \.self) { tag in
Text(tag)
.padding(.horizontal, 12).padding(.vertical, 8)
.background(.blue.opacity(0.15))
.clipShape(Capsule())
.matchedGeometryEffect(id: tag, in: ns)
.onTapGesture {
withAnimation(.spring()) { selected = tag }
}
}
}
if let tag = selected {
VStack(spacing: 12) {
Text(tag).font(.headline)
.matchedGeometryEffect(id: tag, in: ns)
Text("Details about \(tag)…").foregroundStyle(.secondary)
Button("Close") {
withAnimation(.spring()) { selected = nil }
}
}
.padding()
.frame(maxWidth: .infinity)
.background(.thinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 16))
.transition(.opacity.combined(with: .scale))
}
}
.padding()
}
}
7) Task completion “confetti” (simple)
Quick celebratory animation using a temporary overlay.
struct DoneConfetti: View {
@State private var show = false
var body: some View {
ZStack {
Button("Complete Task") {
withAnimation(.spring()) { show = true }
// Hide after a moment
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
withAnimation(.easeOut) { show = false }
}
}
if show {
Image(systemName: "checkmark.seal.fill")
.font(.system(size: 72))
.foregroundStyle(.green)
.transition(.scale.combined(with: .opacity))
.zIndex(1)
}
}
}
}
Hook this into your To‑Do toggle: when a task becomes done, flip a @State showCelebration for that row.
8) Animation cookbook (useful presets)
Ease:
.easeIn,.easeOut,.easeInOut(duration: 0.25)Spring:
.spring(response: 0.35, dampingFraction: 0.7)(snappy)Bouncy iOS‑style:
.interpolatingSpring(stiffness: 200, damping: 20)Repeat:
.repeatForever(autoreverses: true)(remember to start it in.onAppear)
Example shimmer skeleton:
struct Shimmer: View {
@State private var phase: CGFloat = -0.8
var body: some View {
RoundedRectangle(cornerRadius: 12)
.fill(.gray.opacity(0.25))
.overlay(
LinearGradient(stops: [
.init(color: .white.opacity(0.0), location: 0.0),
.init(color: .white.opacity(0.7), location: 0.5),
.init(color: .white.opacity(0.0), location: 1.0)
], startPoint: .topLeading, endPoint: .bottomTrailing)
.offset(x: phase * 200)
)
.onAppear {
withAnimation(.linear(duration: 1.2).repeatForever(autoreverses: false)) {
phase = 0.8
}
}
}
}
🛠 Practice (apply to your To‑Do app)
Row toggle pop: Animate checkmark + slight row scale on completion.
Insert transition: When adding a task, slide it in from the top with
.transition(.move(edge: .top).combined(with: .opacity)).Edit sheet: Animate the “New Task” sheet controls with
.spring()when fields appear/disappear (e.g., show Due Date picker behind a toggle).Matched geometry: Animate a selected task title from the list into a detail header.
Empty state: Animate
ContentUnavailableViewfade in/out as the list becomes empty/non‑empty.
