Day 21 – App Polish & UX Refinements
T
Tuan Beo

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 TaskItem already conforms to Identifiable — 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 .onAppear logic)

  • 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, error

  • UISelectionFeedbackGenerator → 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 changes

  • Keep animation durations short (0.2–0.3s) for responsiveness

  • Avoid animating everything — focus on actions (add/delete/complete)


🛠 Practice

  1. Add VoiceOver labels to all interactive elements in the To-Do app.

  2. Trigger a .success haptic when saving a task.

  3. Create a custom empty state view with an icon + message + call-to-action.

  4. Add .refreshable to simulate “sync” — maybe shuffle task order or reload from storage.

Comments