
Day 21 – App Polish & UX Refinements
Improve performance for smoother lists, add accessibility for VoiceOver users, integrate subtle haptics for feedback and refine UX: empty states, pull-to-refresh, and consistent animations
1️⃣ Performance Tips (List Rendering)
Use Identifiable & ForEach correctly
Our
TaskItemalready conforms toIdentifiable— good ✅Avoid
.id(UUID())in lists unless you want the row to re-render every time
Lazy rendering
SwiftUI’s List and LazyVStack are lazy by default — they only render rows when needed. But:
Keep the row view lightweight (avoid heavy
.onAppearlogic)Use smaller view hierarchies in lists (extract components)
Example:
struct TaskRow: View {
let task: TaskItem
var body: some View {
HStack { /* simple UI here */ }
}
}
Avoid unnecessary @Published changes
If you have a large list:
Don’t modify the entire array unless needed
Modify individual elements by index
2️⃣ Accessibility Pass (VoiceOver)
Add descriptive labels and hints so screen readers understand your UI.
Image(systemName: task.isDone ? "checkmark.circle.fill" : "circle")
.accessibilityLabel(task.isDone ? "Completed" : "Not completed")
.accessibilityHint("Double-tap to toggle task completion")
For entire rows:
HStack {
Text(task.title)
}.accessibilityElement(children: .combine)
Enable Dynamic Type:
Don’t hardcode font sizes — use
.font(.headline)instead of.font(.system(size: 16))Test in Settings → Accessibility → Larger Text
3️⃣ Haptics (Tactile Feedback)
Give subtle feedback for important actions.
import UIKit
func triggerHaptic(style: UIImpactFeedbackGenerator.FeedbackStyle = .medium) {
let generator = UIImpactFeedbackGenerator(style: style)
generator.impactOccurred()
}
// Example: call inside a button action
Button {
withAnimation {
vm.toggleDone(task)
}
triggerHaptic(.light)
} label { /* row UI */ }
Other types:
UINotificationFeedbackGenerator→ success, warning, errorUISelectionFeedbackGenerator→ picker changes
4️⃣ Empty States
Instead of just showing nothing, give users a reason to take action.
if vm.tasks.isEmpty {
VStack(spacing: 12) {
Image(systemName: "tray")
.font(.largeTitle)
.foregroundStyle(.secondary)
Text("No tasks yet").font(.headline)
Text("Tap + to create your first task")
.font(.subheadline)
.foregroundStyle(.secondary)
}
.padding()
.multilineTextAlignment(.center)
}
You can animate it in with:
.transition(.opacity.combined(with: .scale))
5️⃣ Pull-to-Refresh
iOS 15+ supports it out of the box.
List {
ForEach(vm.visibleTasks) { task in /* row */ }
}
.refreshable {
await vm.refreshTasks()
}
For local apps like ours, .refreshable could:
Re-sort tasks
Pull in new tasks from an API (future week)
6️⃣ Consistent Animations
Use
.animation(.easeInOut, value: vm.tasks)for list changesKeep animation durations short (0.2–0.3s) for responsiveness
Avoid animating everything — focus on actions (add/delete/complete)
🛠 Practice
Add VoiceOver labels to all interactive elements in the To-Do app.
Trigger a
.successhaptic when saving a task.Create a custom empty state view with an icon + message + call-to-action.
Add
.refreshableto simulate “sync” — maybe shuffle task order or reload from storage.
