[Refactor]: Migrate Contest feature to MVVM architecture with Service/Repository layers
@harshitraj1236 is already working on this.
Since Apr 12, 2026.
- Dominant language
- Swift
- Stars
- 0
- Forks
- 4
- PR merge metrics
- No merged PRs in 30d
Description
### Description:
The **Problem** feature follows a clean, testable MVVM architecture:
```
ProblemService → ProblemRepository (actor, caching) → ProblemListViewModel → ProblemListView
```
However, the **Contest** feature has no such layering. All networking, state management, and UI logic are tightly coupled inside `ContestListView` and its extensions:
- `ContestAPI.swift` — Network calls defined as View extensions (`loadContests()`, `fetchContests()`)
- `ContestHelpers.swift` — Filtering logic defined as a View extension
- `ContestListView.swift` — Holds `@State` variables directly for `contests`, `searchText`, `isRefreshing`, and `errorMessage`
This makes the Contest feature untestable, inconsistent with the rest of the codebase, and harder to maintain.
### Current Behavior:
- `ContestListView` directly manages `@State` properties for contest data, loading, and errors.
- API calls are made as View extension methods, not through a service layer.
- There is no caching — every time the user switches tabs, contests are re-fetched.
- Error handling uses `UIAlertController` via a `showAlert()` function that accesses `UIApplication.shared` directly.
### Feature Requirements:
- **Create `ContestService`** (in `CForge/Services/`) — Handles raw API calls using `async/await`. Must follow `ProblemService` patterns: protocol-based, structured `AppLog` logging, proper `NetworkError` usage.
- **Create `ContestRepository`** (in `CForge/Repositories/`) — Swift `actor` with in-memory caching, cache TTL, and request deduplication. Mirror `ProblemRepository`.
- **Create `ContestListViewModel`** (in `CForge/ViewModels/`) — `@MainActor`, `ObservableObject` with `ViewState` enum (`idle`, `loading`, `loaded([CFContest])`, `error(String)`). Handles filtering internally.
- **Refactor `ContestListView`** — Remove all `@State` data properties. Use `@StateObject var viewModel`.
- **Delete `ContestAPI.swift`** — Networking moves to `ContestService`.
- **Delete `ContestHelpers.swift`** — Filtering moves to `ContestListViewModel` or a `ContestFilterEngine`.
- Remove `showAlert()` UIKit hack — use SwiftUI `.alert` pattern (already partially present).
### Key Files:
| Action | File |
|--------|------|
| **CREATE** | `CForge/Services/ContestService.swift` |
| **CREATE** | `CForge/Repositories/ContestRepository.swift` |
| **CREATE** | `CForge/ViewModels/ContestListViewModel.swift` |
| **MODIFY** | `CForge/Views/Contest/ContestListView.swift` |
| **MODIFY** | `CForge/Views/Contest/ContestModels.swift` (un-nest from `ContestListView`) |
| **DELETE** | `CForge/Views/Contest/ContestAPI.swift` |
| **DELETE** | `CForge/Views/Contest/ContestHelpers.swift` |
### Expected Outcome:
- Contest feature follows `Service → Repository → ViewModel → View`, matching the Problem feature.
- All networking uses structured `async/await` with `AppLog` and `NetworkError`.
- Contest data is cached to avoid unnecessary API calls on tab switches.
- Models are standalone structs, not nested inside View extensions.
- UIKit `showAlert()` hack is removed in favor of pure SwiftUI.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.