Skip to content

Latest commit

Β 

History

History
567 lines (401 loc) Β· 13 KB

File metadata and controls

567 lines (401 loc) Β· 13 KB

MVVM-C Architecture Guide

MVVM-C = Model-View-ViewModel + Coordinator

A teaching guide to understanding the Coordinator pattern and how it enhances MVVM.


🎯 What is MVVM-C?

MVVM-C extends the MVVM pattern by adding a Coordinator layer that handles navigation logic.

Traditional MVVM Problem

In traditional MVVM, ViewModels often handle both business logic AND navigation:

class ViewModel {
    func didSelectItem() {
        // Business logic
        processItem()

        // Navigation logic - MIXED RESPONSIBILITY!
        navigationController?.push(DetailView())
    }
}

Problems:

  • ViewModels know about navigation APIs
  • Hard to test (need to mock navigation)
  • Can't reuse ViewModel with different navigation
  • Violates Single Responsibility Principle

MVVM-C Solution

MVVM-C separates navigation into a dedicated Coordinator:

class Coordinator {
    func navigateToItem(_ item: Item) {
        // Navigation logic ONLY
        navigationController?.push(DetailView(item: item))
    }
}

class ViewModel {
    func processItem() {
        // Business logic ONLY
        // No navigation concerns!
    }
}

Benefits:

  • βœ… Clear separation of concerns
  • βœ… ViewModels are easier to test
  • βœ… ViewModels can be reused with different navigation
  • βœ… Navigation logic is centralized and consistent

πŸ“ Architecture Layers

1. Model Layer (Models/)

Pure data structures - exactly the same as MVVM.

MVVMCListItem.swift       - Item data model
MVVMCSidebarCategory.swift - Category enum

No changes from traditional MVVM - models are always pure data.

2. ViewModel Layer (ViewModels/)

Business logic ONLY - no navigation!

MVVMCSidebarViewModel.swift      - Sidebar data operations
MVVMCContentListViewModel.swift  - Content list data operations
MVVMCSettingsViewModel.swift     - Settings data operations

Key Difference from MVVM:

  • ❌ ViewModels don't manage selection state
  • ❌ ViewModels don't handle navigation
  • βœ… ViewModels focus purely on data operations

3. Coordinator Layer (Coordinators/) πŸ†•

Navigation logic - this is the new layer!

MVVMCAppCoordinator.swift - Main navigation coordinator

Responsibilities:

  • Owns navigation state (selectedCategory, selectedItem)
  • Owns ViewModels (coordinates data operations)
  • Provides navigation methods (navigateToCategory, navigateToItem)
  • Handles deep linking and complex navigation flows

4. View Layer (Views/)

SwiftUI views - displays UI and delegates to Coordinator

MVVMCContentView.swift          - Root navigation view
MVVMCSidebarView.swift          - Sidebar
MVVMCContentListView.swift      - Content list
MVVMCSettingsContentView.swift  - Settings
MVVMCDetailView.swift           - Detail view
MVVMCOverviewTab.swift          - Overview tab
MVVMCDetailsTab.swift           - Details tab
MVVMCOptionsTab.swift           - Options tab
MVVMCInfoRow.swift              - Info row component
MVVMCDetailRow.swift            - Detail row component

Key Pattern:

  • Views receive the Coordinator (for navigation)
  • Views receive ViewModels (for data operations)
  • Views call Coordinator methods when navigating
  • Views call ViewModel methods for data operations

πŸ”„ Data Flow

MVVM Data Flow (Without Coordinator)

graph TD
    A[View] -->|reads state| B[ViewModel]
    A -->|mutates binding| B
    B -->|notifies changes| A
    B -->|handles navigation| C[Navigation API]
Loading

Issues:

  • ViewModel knows about navigation
  • Mixed responsibilities

MVVM-C Data Flow (With Coordinator)

graph TD
    A[View] -->|navigation actions| B[Coordinator]
    A -->|data operations| C[ViewModel]
    B -->|owns| C
    B -->|owns state| D[Navigation State]
    C -->|data changes| A
    D -->|state changes| A
Loading

Benefits:

  • Clear separation
  • ViewModel is pure business logic
  • Coordinator handles all navigation

πŸ—οΈ Component Relationships

graph TB
    ContentView[MVVMCContentView]
    Coordinator[MVVMCAppCoordinator]

    SidebarVM[MVVMCSidebarViewModel]
    ContentVM[MVVMCContentListViewModel]
    SettingsVM[MVVMCSettingsViewModel]

    SidebarView[MVVMCSidebarView]
    ContentList[MVVMCContentListView]
    SettingsView[MVVMCSettingsContentView]
    DetailView[MVVMCDetailView]

    ContentView -->|owns| Coordinator

    Coordinator -->|owns| SidebarVM
    Coordinator -->|owns| ContentVM
    Coordinator -->|owns| SettingsVM
    Coordinator -->|manages| NavState[Navigation State]

    ContentView -->|creates| SidebarView
    ContentView -->|creates| ContentList
    ContentView -->|creates| SettingsView
    ContentView -->|creates| DetailView

    SidebarView -->|receives| Coordinator
    ContentList -->|receives| Coordinator
    SettingsView -->|receives| SettingsVM
    DetailView -->|receives| Model[MVVMCListItem]
Loading

πŸ’‘ When to Use MVVM-C

βœ… Use MVVM-C When

  1. Complex Navigation

    • Multi-step flows (wizards, onboarding)
    • Deep linking requirements
    • Tab-based navigation with state
    • Modal presentations with context
  2. Testability is Critical

    • Need to test ViewModels without navigation mocks
    • Want to verify navigation logic separately
    • Building for CI/CD with comprehensive tests
  3. Reusable ViewModels

    • Same ViewModel used in different contexts
    • Different navigation patterns (iPhone vs iPad)
    • Multiple entry points to same functionality
  4. Large Teams

    • Clear boundaries help parallel development
    • Easier code reviews (navigation vs business logic)
    • Onboarding new developers is clearer

❌ Don't Use MVVM-C When

  1. Simple Apps

    • Single-screen apps
    • Minimal navigation
    • Prototype/MVP phase
  2. Learning SwiftUI

    • Start with ViewOnly or MVVM first
    • Master the basics before adding coordinators
  3. Overhead Concerns

    • Very small team/solo developer
    • Rapid prototyping phase
    • Time constraints

πŸ†š MVVM vs MVVM-C Comparison

Code Comparison

MVVM (Without Coordinator)

// ViewModel owns navigation state
@Observable
class AppViewModel {
    var selectedCategory: Category? = .category1
    var selectedItem: Item? = nil
}

// View binds directly to ViewModel
struct SidebarView: View {
    @Binding var selectedCategory: Category?

    var body: some View {
        List(selection: $selectedCategory) { // Direct binding
            // ...
        }
    }
}

MVVM-C (With Coordinator)

// Coordinator owns navigation state
@Observable
class Coordinator {
    var selectedCategory: Category? = .category1
    var selectedItem: Item? = nil

    // Explicit navigation methods
    func navigateToCategory(_ category: Category) {
        selectedCategory = category
        // Could add analytics, validation, etc.
    }
}

// View calls Coordinator methods
struct SidebarView: View {
    let coordinator: Coordinator

    var body: some View {
        List {
            ForEach(categories) { category in
                Button {
                    coordinator.navigateToCategory(category) // Method call
                } label: {
                    Text(category.name)
                }
            }
        }
    }
}

Comparison Table

Aspect MVVM MVVM-C
Navigation State In ViewModel In Coordinator
Navigation Logic In ViewModel In Coordinator
ViewModel Responsibility Data + Navigation Data only
Testing ViewModels Need navigation mocks No navigation dependencies
View Simplicity Direct bindings Method calls
Deep Linking Complex Straightforward
Code Organization 3 layers 4 layers
Learning Curve Medium Higher
Best For Most apps Complex navigation

πŸ“ Key Files Explained

MVVMCAppCoordinator.swift

The central navigation coordinator.

What it does:

  • Owns all navigation state
  • Owns all ViewModels
  • Provides navigation methods
  • Handles deep linking

Key properties:

var selectedCategory: MVVMCSidebarCategory?
var selectedItem: MVVMCListItem?
let sidebarViewModel = MVVMCSidebarViewModel()
let contentListViewModel = MVVMCContentListViewModel()
let settingsViewModel = MVVMCSettingsViewModel()

Key methods:

func navigateToCategory(_ category: MVVMCSidebarCategory)
func navigateToItem(_ item: MVVMCListItem)
func clearItemSelection()
func resetNavigation()

MVVMCContentView.swift

The root view that owns the Coordinator.

What it does:

  • Creates the Coordinator instance
  • Passes Coordinator to child views
  • Renders the 3-pane NavigationSplitView

Key pattern:

struct MVVMCContentView: View {
    @State private var coordinator = MVVMCAppCoordinator()

    var body: some View {
        NavigationSplitView {
            MVVMCSidebarView(coordinator: coordinator)
        } content: {
            // Content based on coordinator.selectedCategory
        } detail: {
            // Detail based on coordinator.selectedItem
        }
    }
}

ViewModels (MVVMCSidebarViewModel, etc.)

Pure business logic - no navigation!

What they do:

  • Manage data for their domain
  • Provide data access methods
  • Handle data operations (add, delete, update)

What they DON'T do:

  • ❌ Manage selection state
  • ❌ Handle navigation
  • ❌ Know about Coordinators

πŸ§ͺ Testing Benefits

MVVM Testing (Without Coordinator)

func testItemSelection() {
    let viewModel = AppViewModel()
    let mockNav = MockNavigationController() // Need to mock!
    viewModel.navigationController = mockNav

    viewModel.selectItem(item)

    XCTAssertTrue(mockNav.didPushDetailView)
}

Problems:

  • Need to mock navigation
  • ViewModel depends on navigation API
  • Tests are coupled to navigation implementation

MVVM-C Testing (With Coordinator)

func testItemDataProcessing() {
    let viewModel = ContentListViewModel()

    viewModel.addItem(title: "Test", subtitle: "Test")

    XCTAssertEqual(viewModel.items.count, 1)
    // No navigation mocking needed!
}

func testCoordinatorNavigation() {
    let coordinator = AppCoordinator()

    coordinator.navigateToCategory(.category2)

    XCTAssertEqual(coordinator.selectedCategory, .category2)
    XCTAssertNil(coordinator.selectedItem) // Cleared on category change
}

Benefits:

  • ViewModels test pure business logic
  • Coordinator tests navigation logic separately
  • No mocking needed
  • Tests are simpler and more focused

πŸŽ“ Learning Path

Coming from MVVM?

Key Mental Shifts:

  1. Navigation is separate from business logic

    • Don't put navigation in ViewModels
    • Create Coordinator methods instead
  2. Coordinators own ViewModels

    • Not the other way around
    • Coordinator is the "single source of truth"
  3. Views call methods, not mutate bindings

    • coordinator.navigateToItem() instead of selectedItem = item
    • More explicit, easier to understand

Learning Steps

  1. Understand MVVM first (see MVVM_Architecture.md)
  2. Identify navigation logic in your ViewModels
  3. Extract navigation to a Coordinator
  4. Update views to call Coordinator methods
  5. Add navigation features (deep linking, etc.)

πŸš€ When You're Ready for More

Advanced Patterns

  1. Child Coordinators

    • One coordinator per major flow
    • Main coordinator coordinates child coordinators
  2. Protocol-Based Coordinators

    • Define coordination protocols
    • Easier to test and mock
  3. Coordinator Factory

    • Centralize coordinator creation
    • Dependency injection
  4. Coordinator State Persistence

    • Save/restore navigation state
    • Handle app termination/restart

πŸ“š Further Reading


βœ… Summary

MVVM-C = MVVM + Coordinator

Coordinator handles:

  • Navigation state
  • Navigation methods
  • Deep linking
  • Navigation flow logic

ViewModels handle:

  • Business logic only
  • Data operations
  • No navigation!

Benefits:

  • Cleaner separation
  • Easier testing
  • More flexible
  • Better for complex apps

When to use:

  • Complex navigation
  • Critical testability
  • Large teams
  • Reusable ViewModels

When NOT to use:

  • Simple apps
  • Learning phase
  • Rapid prototyping
  • Solo projects with time constraints

This is a teaching repository. The MVVM-C pattern is demonstrated with extensive comments and documentation to help you learn the concepts.