MVVM Architecture Analysis Report
Date: 2026-01-02 Scope: Evaluate whether MVVM would improve data management in Minuta
Executive Summary
After thorough analysis of the codebase, MVVM would NOT be a better architectural approach for Minuta. The current “Environment-Based State with Actor Services” architecture is well-suited for the app’s scope and complexity. Adopting MVVM would add boilerplate without meaningful benefits.
Current Architecture Overview
Pattern: Centralized State + Protocol Services
┌─────────────────────────────────────────────────────────────┐
│ Views │
│ (ContentView, HistorySection, RecordEditor, etc.) │
│ @EnvironmentObject var appState: AppState │
└────────────────────────┬────────────────────────────────────┘
│ calls methods, reads @Published
▼
┌─────────────────────────────────────────────────────────────┐
│ AppState (@MainActor) │
│ - tags, todayRecords, runningTimers, pendingDeletions │
│ - loadData(), startTimer(), stopTimer(), updateRecord() │
│ - Coordinates services, manages UI state │
└────────────────────────┬────────────────────────────────────┘
│ async calls
▼
┌─────────────────────────────────────────────────────────────┐
│ Services (Actors) │
│ TimeTrackingService ──▶ LocalFileStorageService │
│ Protocol-based for testability │
└────────────────────────┬────────────────────────────────────┘
│
▼
JSON Files (disk) Key Characteristics
| Aspect | Implementation |
|---|---|
| State Container | Single AppState class (~365 lines) |
| State Distribution | @EnvironmentObject across all views |
| Business Logic | Actor-based services with protocols |
| View Logic | Local @State + computed properties |
| Testability | Protocol mocks for services |
| Concurrency | Swift actors + @MainActor |
What MVVM Would Look Like
Hypothetical MVVM Structure
┌────────────────────────────────────────────────────────────┐
│ Views │
│ @StateObject var viewModel: HistorySectionViewModel │
└────────────────────────┬───────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────┐
│ ViewModels │
│ HistorySectionViewModel, TodaySectionViewModel, │
│ RecordEditorViewModel, SettingsViewModel, etc. │
│ Each wraps portion of AppState logic │
└────────────────────────┬───────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────┐
│ AppState / Repository Layer │
└────────────────────────┬───────────────────────────────────┘
│
▼
Services (unchanged) Required Changes for MVVM
Create ~8-10 ViewModels:
ContentViewModelHistorySectionViewModelTodaySectionViewModelRunningTimersSectionViewModelRecordEditorViewModelSettingsViewModelTagFilterViewModelDateRangePickerViewModelExportViewModel
Split AppState responsibilities across ViewModels
Add state synchronization between ViewModels (currently free with shared AppState)
Update all views to use
@StateObjector@ObservedObject
Detailed Comparison
1. Testability
| Aspect | Current | MVVM |
|---|---|---|
| Service layer | Fully testable via protocols | Same |
| Business logic | Testable via AppState methods | Testable via ViewModels |
| View logic | Harder to test (in views) | Easier to test (in ViewModels) |
| Test count needed | ~20 service tests | ~20 service + ~30 ViewModel tests |
Verdict: MVVM slightly better for view logic testing, but current service-level testing covers 90%+ of business logic.
Evidence: Existing test coverage in Shared/Tests/ includes:
TimeTrackingServiceTests.swiftLocalFileStorageServiceTests.swiftExportServiceTests.swiftImportServiceTests.swiftMockTimeTrackingService.swift(166 lines)
View-specific logic is minimal and tested via UI tests.
2. Code Organization
| Aspect | Current | MVVM |
|---|---|---|
| File count | 25 view files + 1 AppState | 25 view files + 10 ViewModels |
| Lines of code | ~3500 view code | ~3500 view + ~1500 ViewModel |
| Cognitive load | One place for state | Scattered across ViewModels |
| Navigation | Find AppState method | Find which ViewModel owns what |
Verdict: Current is simpler. Single AppState is easier to navigate than distributed ViewModels.
3. State Synchronization
| Aspect | Current | MVVM |
|---|---|---|
| Tag changes | Auto-synced via @Published | Need manual sync or shared state |
| Record updates | Auto-synced | Need pub/sub or shared state |
| Cross-view coordination | Free | Extra machinery needed |
Example problem MVVM creates:
When a tag is renamed in Settings, these views need updates:
- TodaySectionView (shows tag names)
- HistorySection (shows tag names)
- RunningTimersSection (shows tag names)
- TagFilterView (shows filter buttons)
Current solution: One @Published var tags: [Tag] in AppState.
MVVM solution: Either:
- Share AppState anyway (defeating MVVM purpose)
- Add NotificationCenter/Combine pub/sub
- Inject shared TagsRepository into every ViewModel
4. View Logic Complexity
Analyzing current view logic:
| View | Local @State vars | Logic in view |
|---|---|---|
| ContentView | 10 | Minimal - delegates to sections |
| HistorySection | 5 | Filtering, grouping (cached) |
| TodaySectionView | 3 | Simple filter + cache |
| RecordEditor | 9 | Form state management |
| TagFilterView | 2 | Sort + filter |
Observation: View logic is already well-contained:
- Heavy lifting in views is already cached (
@State+onChange) - Complex operations delegated to services
- RecordEditor has most logic but it’s form-specific
MVVM impact: Would move ~100-150 lines per complex view to ViewModels. Marginal benefit for this app size.
5. Performance
| Aspect | Current | MVVM |
|---|---|---|
| Update propagation | Single @Published path | Multiple ViewModel updates |
| Memory | One AppState instance | Multiple ViewModel instances |
| SwiftUI diffing | One source of truth | Multiple sources |
Evidence from docs/800-performance/801-optimization-plan.md:
- App handles 750 records with 2.0s launch, 8.8s scroll
- Performance issues were structural (Grid vs List), not architectural
- Optimizations were in view layer (caching) and service layer (selective loading)
Verdict: Current architecture didn’t cause performance issues. MVVM wouldn’t help performance.
6. Scalability Considerations
Current pain points (if app grew):
- AppState is 365 lines - manageable but could grow
- Some views have 10+
@Statevars - No clear ownership boundaries for new features
MVVM would help if:
- App had 50+ screens (currently ~5 main areas)
- Multiple developers needed clear ownership
- Complex view-specific business logic needed testing
- Features had isolated state (current state is highly interconnected)
Reality check: Minuta is a focused time-tracking tool, not a social network. Feature scope is inherently limited.
Specific Code Analysis
AppState Responsibilities (MinutaApp.swift:282-645)
Category Lines Complexity
─────────────────────────────────────────
Properties 25 Low
Initialization 35 Low
CRUD - Tags 80 Medium
CRUD - Records 60 Medium
Timer operations 30 Low
Utilities 20 Low
State helpers 15 Low
─────────────────────────────────────────
Total 265 Medium This is a reasonable size for a single state manager. Splitting would fragment related operations.
View State Analysis (sample views)
HistorySection.swift:
@Binding var historyStartDate: Date // From parent
@Binding var historyEndDate: Date // From parent
@Binding var historyRecords: [TimeRecord] // From parent
@State private var filteredRecords: [TimeRecord] = [] // Cache
@State private var cachedMonthGroups: [MonthGroup] = [] // Cache
@State private var visibility: HistoryVisibility // Cache
@State private var cachedTotalDuration: TimeInterval = 0 // Cache All @State here is caching/memoization, not business logic. This is appropriate view-level concern.
RecordEditor.swift:
@State private var isEditing = false
@State private var editedComment: String = ""
@State private var tagInput: String = ""
@State private var editedStartTime: Date = Date()
@State private var editedEndTime: Date = Date()
@State private var showImagePicker = false
@State private var selectedImage: UIImage?
@State private var isAddingImage = false
@State private var currentRecord: TimeRecord? This is form state - standard for any form component. MVVM would move this to RecordEditorViewModel, but:
- Still needs to sync with view for pickers
- Validation is simple (end > start)
- Save logic is one async call
Alternative Improvements (Without MVVM)
If refactoring is desired, consider these lighter-weight changes:
1. Extract Tag Management
// TagManager.swift (optional extraction)
@MainActor
class TagManager: ObservableObject {
@Published var tags: [Tag] = []
@Published private(set) var tagsByID: [UUID: Tag] = [:]
func rename(_ tag: Tag, to name: String) async { ... }
func archive(_ tag: Tag) async { ... }
// etc.
} Benefit: Isolates tag CRUD without full MVVM overhead.
2. Domain-Specific View Extensions
extension TimeRecord {
func formatTimeRange() -> String { ... }
func formatDuration() -> String { ... }
} Benefit: Moves formatting out of views without ViewModels.
3. Dedicated Form State Containers
struct RecordEditState {
var comment: String
var tagInput: String
var startTime: Date
var endTime: Date
init(from record: TimeRecord, tags: [Tag]) { ... }
func toRecord(_ original: TimeRecord) -> TimeRecord { ... }
} Benefit: Encapsulates form logic without full ViewModel.
When to Reconsider MVVM
MVVM would become beneficial if:
- Team size increases: Multiple developers need clear ownership boundaries
- Feature explosion: 20+ screens with complex isolated logic
- Unit test requirements: Mandate for 80%+ code coverage including view logic
- Complex navigation: Deep navigation stacks with state preservation needs
- Platform split: Separate Mac/iOS implementations sharing ViewModels
Conclusion
| Factor | Weight | Current | MVVM |
|---|---|---|---|
| Simplicity | High | Better | Worse |
| Testability | Medium | Good | Better |
| Maintainability | High | Good | Neutral |
| Performance | Medium | Good | Neutral |
| Boilerplate | Medium | Low | High |
| Learning curve | Low | Low | Medium |
Recommendation: Keep current architecture.
The “Environment-Based State with Actor Services” pattern is:
- Appropriate for app complexity
- Already testable at service level
- Performant with proper caching
- Simpler to understand and modify
MVVM would add ~1500 lines of ViewModel code, require state synchronization machinery, and fragment currently cohesive logic - all without solving any actual problems the app has.
Related Documentation
- 101-overview - Current architecture overview
- 801-optimization-plan - Performance optimizations (not architecture-related)
- 402-ios - UI structure details