Redesign achievements progress bar with milestone circles

Replaces the simple progress bar with a custom milestone bar showing
individual circles for each achievement, filled when unlocked with a
checkmark indicator.

Fixes #27

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
alexandrev-tibco
2026-02-11 23:57:58 +01:00
parent b4615ac558
commit 151eb0e662
@@ -705,18 +705,18 @@ struct AchievementsView: View {
private var headerCard: some View { private var headerCard: some View {
let total = max(achievementStatuses.count, 1) let total = max(achievementStatuses.count, 1)
let unlockedCount = unlockedAchievements.count let unlockedCount = unlockedAchievements.count
let progress = Double(unlockedCount) / Double(total)
return VStack(alignment: .leading, spacing: 8) { return VStack(alignment: .leading, spacing: 8) {
Text(String(localized: "achievements_progress_title")) Text(String(localized: "achievements_progress_title"))
.font(.headline) .font(.headline)
ProgressView(value: progress)
.tint(.appSecondary) AchievementMilestoneBar(statuses: achievementStatuses)
Text( Text(
String( String(
format: NSLocalizedString("achievements_unlocked_count", comment: ""), format: NSLocalizedString("achievements_unlocked_count", comment: ""),
unlockedCount, unlockedCount,
achievementStatuses.count total
) )
) )
.font(.subheadline) .font(.subheadline)
@@ -833,6 +833,61 @@ private extension MonthlyCheckInView {
} }
// MARK: - Achievement Milestone Bar
struct AchievementMilestoneBar: View {
let statuses: [MonthlyCheckInAchievementStatus]
var body: some View {
let total = max(statuses.count, 1)
let unlockedCount = statuses.filter(\.isUnlocked).count
let progress = Double(unlockedCount) / Double(total)
GeometryReader { geo in
let barWidth = geo.size.width
let barHeight: CGFloat = 6
ZStack(alignment: .leading) {
// Background track
Capsule()
.fill(Color.gray.opacity(0.2))
.frame(height: barHeight)
// Filled track
Capsule()
.fill(Color.appSecondary)
.frame(width: barWidth * progress, height: barHeight)
// Milestone circles
ForEach(Array(statuses.enumerated()), id: \.element.id) { index, status in
let position = total == 1
? barWidth / 2
: barWidth * Double(index) / Double(total - 1)
Circle()
.fill(status.isUnlocked ? Color.appSecondary : Color.gray.opacity(0.3))
.frame(width: 14, height: 14)
.overlay(
Circle()
.stroke(Color(.systemBackground), lineWidth: 2)
)
.overlay(
Group {
if status.isUnlocked {
Image(systemName: "checkmark")
.font(.system(size: 7, weight: .bold))
.foregroundColor(.white)
}
}
)
.position(x: position, y: geo.size.height / 2)
}
}
}
.frame(height: 20)
}
}
#Preview { #Preview {
NavigationStack { NavigationStack {
MonthlyCheckInView() MonthlyCheckInView()