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:
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user