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

AspectImplementation
State ContainerSingle AppState class (~365 lines)
State Distribution@EnvironmentObject across all views
Business LogicActor-based services with protocols
View LogicLocal @State + computed properties
TestabilityProtocol mocks for services
ConcurrencySwift 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

  1. Create ~8-10 ViewModels:

    • ContentViewModel
    • HistorySectionViewModel
    • TodaySectionViewModel
    • RunningTimersSectionViewModel
    • RecordEditorViewModel
    • SettingsViewModel
    • TagFilterViewModel
    • DateRangePickerViewModel
    • ExportViewModel
  2. Split AppState responsibilities across ViewModels

  3. Add state synchronization between ViewModels (currently free with shared AppState)

  4. Update all views to use @StateObject or @ObservedObject


Detailed Comparison

1. Testability

AspectCurrentMVVM
Service layerFully testable via protocolsSame
Business logicTestable via AppState methodsTestable via ViewModels
View logicHarder 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.swift
  • LocalFileStorageServiceTests.swift
  • ExportServiceTests.swift
  • ImportServiceTests.swift
  • MockTimeTrackingService.swift (166 lines)

View-specific logic is minimal and tested via UI tests.

2. Code Organization

AspectCurrentMVVM
File count25 view files + 1 AppState25 view files + 10 ViewModels
Lines of code~3500 view code~3500 view + ~1500 ViewModel
Cognitive loadOne place for stateScattered across ViewModels
NavigationFind AppState methodFind which ViewModel owns what

Verdict: Current is simpler. Single AppState is easier to navigate than distributed ViewModels.

3. State Synchronization

AspectCurrentMVVM
Tag changesAuto-synced via @PublishedNeed manual sync or shared state
Record updatesAuto-syncedNeed pub/sub or shared state
Cross-view coordinationFreeExtra 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:

ViewLocal @State varsLogic in view
ContentView10Minimal - delegates to sections
HistorySection5Filtering, grouping (cached)
TodaySectionView3Simple filter + cache
RecordEditor9Form state management
TagFilterView2Sort + 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

AspectCurrentMVVM
Update propagationSingle @Published pathMultiple ViewModel updates
MemoryOne AppState instanceMultiple ViewModel instances
SwiftUI diffingOne source of truthMultiple 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+ @State vars
  • 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:

  1. Team size increases: Multiple developers need clear ownership boundaries
  2. Feature explosion: 20+ screens with complex isolated logic
  3. Unit test requirements: Mandate for 80%+ code coverage including view logic
  4. Complex navigation: Deep navigation stacks with state preservation needs
  5. Platform split: Separate Mac/iOS implementations sharing ViewModels

Conclusion

FactorWeightCurrentMVVM
SimplicityHighBetterWorse
TestabilityMediumGoodBetter
MaintainabilityHighGoodNeutral
PerformanceMediumGoodNeutral
BoilerplateMediumLowHigh
Learning curveLowLowMedium

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