Files
FrenoCorp/Lendair/ViewModels/ChallengeViewModel.swift
Senior Engineer 88d57a3389 Add Phase 2 community features: clubs and challenges (FRE-4664)
Implement full MVVM stack for two new community features:

Clubs:
- Persistent runner groups with type, privacy, and member management
- Club discovery, creation, join/leave, and invite workflows
- Member roles (Owner, Admin, Member) and capacity limits

Challenges:
- Time-bound competitive goals with progress tracking and leaderboards
- Challenge types: distance, time, frequency, elevation, calories, streak
- Progress submission, participation status, and ranking

Files:
- Models: Club.swift, Challenge.swift
- Services: ClubService.swift, ChallengeService.swift
- ViewModels: ClubViewModel.swift, ChallengeViewModel.swift
- Views: ClubsView.swift, ClubDetailView.swift, ChallengesView.swift, ChallengeDetailView.swift
- Tests: ClubServiceTests.swift, ChallengeServiceTests.swift
- Updated README.md with new feature documentation
2026-05-03 19:10:34 -04:00

166 lines
5.4 KiB
Swift

import Foundation
import SwiftUI
@MainActor
class ChallengeViewModel: ObservableObject {
@Published var challenges: [Challenge] = []
@Published var selectedChallenge: Challenge?
@Published var leaderboard: [LeaderboardEntry] = []
@Published var isLoading: Bool = false
@Published var error: ChallengeError?
@Published var filter: ChallengeFilter = ChallengeFilter()
private let service: ChallengeServiceProtocol
init(service: ChallengeServiceProtocol = ChallengeService()) {
self.service = service
}
func fetchChallenges() async {
isLoading = true
error = nil
defer { isLoading = false }
do {
challenges = try await service.listChallenges(filter: filter)
} catch let error as ChallengeError {
self.error = error
} catch {
print("Failed to fetch challenges: \(error)")
}
}
func selectChallenge(id: String) async {
isLoading = true
error = nil
defer { isLoading = false }
do {
let result = try await service.getChallenge(id: id)
selectedChallenge = result.challenge
if let index = challenges.firstIndex(where: { $0.id == id }) {
challenges[index] = result.challenge
objectWillChange.send()
}
} catch let error as ChallengeError {
self.error = error
} catch {
print("Failed to get challenge: \(error)")
}
}
func createChallenge(request: CreateChallengeRequest) async -> Challenge? {
isLoading = true
error = nil
defer { isLoading = false }
do {
let challenge = try await service.createChallenge(request: request)
challenges.insert(challenge, at: 0)
objectWillChange.send()
return challenge
} catch let error as ChallengeError {
self.error = error
return nil
} catch {
print("Failed to create challenge: \(error)")
return nil
}
}
func updateChallenge(id: String, request: UpdateChallengeRequest) async {
isLoading = true
error = nil
defer { isLoading = false }
do {
let updatedChallenge = try await service.updateChallenge(id: id, request: request)
if let index = challenges.firstIndex(where: { $0.id == id }) {
challenges[index] = updatedChallenge
objectWillChange.send()
}
if selectedChallenge?.id == id {
selectedChallenge = updatedChallenge
}
} catch let error as ChallengeError {
self.error = error
} catch {
print("Failed to update challenge: \(error)")
}
}
func joinChallenge(id: String) async {
do {
try await service.joinChallenge(id: id)
if let index = challenges.firstIndex(where: { $0.id == id }) {
challenges[index].participationStatus = .participating
challenges[index].participantCount += 1
objectWillChange.send()
}
} catch {
print("Failed to join challenge: \(error)")
}
}
func leaveChallenge(id: String) async {
do {
try await service.leaveChallenge(id: id)
if let index = challenges.firstIndex(where: { $0.id == id }) {
challenges[index].participationStatus = .notParticipating
challenges[index].participantCount = max(0, challenges[index].participantCount - 1)
objectWillChange.send()
}
} catch {
print("Failed to leave challenge: \(error)")
}
}
func fetchLeaderboard(challengeId: String) async {
isLoading = true
error = nil
defer { isLoading = false }
do {
leaderboard = try await service.getLeaderboard(challengeId: challengeId)
} catch let error as ChallengeError {
self.error = error
} catch {
print("Failed to fetch leaderboard: \(error)")
}
}
func submitProgress(challengeId: String, progress: ProgressSubmission) async {
do {
let result = try await service.submitProgress(challengeId: challengeId, progress: progress)
if let index = challenges.firstIndex(where: { $0.id == challengeId }) {
challenges[index].userProgress = result.progress
objectWillChange.send()
}
if selectedChallenge?.id == challengeId {
selectedChallenge?.userProgress = result.progress
}
} catch {
print("Failed to submit progress: \(error)")
}
}
var activeChallenges: [Challenge] {
challenges.filter { $0.isActive }.sorted { $0.endDate < $1.endDate }
}
var upcomingChallenges: [Challenge] {
challenges.filter { $0.isUpcoming }.sorted { $0.startDate < $1.startDate }
}
var completedChallenges: [Challenge] {
challenges.filter { $0.isCompleted }.sorted { $0.endDate > $1.endDate }
}
var userChallenges: [Challenge] {
challenges.filter { $0.participationStatus == .participating }
}
var challengeTypes: [ChallengeType] { ChallengeType.allCases }
var challengeStatuses: [ChallengeStatus] { ChallengeStatus.allCases }
}