MVVM-C = Model-View-ViewModel + Coordinator
A teaching guide to understanding the Coordinator pattern and how it enhances MVVM.
MVVM-C extends the MVVM pattern by adding a Coordinator layer that handles navigation logic.
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 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
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.
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
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
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
graph TD
A[View] -->|reads state| B[ViewModel]
A -->|mutates binding| B
B -->|notifies changes| A
B -->|handles navigation| C[Navigation API]
Issues:
- ViewModel knows about navigation
- Mixed responsibilities
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
Benefits:
- Clear separation
- ViewModel is pure business logic
- Coordinator handles all navigation
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]
-
Complex Navigation
- Multi-step flows (wizards, onboarding)
- Deep linking requirements
- Tab-based navigation with state
- Modal presentations with context
-
Testability is Critical
- Need to test ViewModels without navigation mocks
- Want to verify navigation logic separately
- Building for CI/CD with comprehensive tests
-
Reusable ViewModels
- Same ViewModel used in different contexts
- Different navigation patterns (iPhone vs iPad)
- Multiple entry points to same functionality
-
Large Teams
- Clear boundaries help parallel development
- Easier code reviews (navigation vs business logic)
- Onboarding new developers is clearer
-
Simple Apps
- Single-screen apps
- Minimal navigation
- Prototype/MVP phase
-
Learning SwiftUI
- Start with ViewOnly or MVVM first
- Master the basics before adding coordinators
-
Overhead Concerns
- Very small team/solo developer
- Rapid prototyping phase
- Time constraints
// 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
// ...
}
}
}// 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)
}
}
}
}
}| 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 |
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()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
}
}
}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
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
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
Key Mental Shifts:
-
Navigation is separate from business logic
- Don't put navigation in ViewModels
- Create Coordinator methods instead
-
Coordinators own ViewModels
- Not the other way around
- Coordinator is the "single source of truth"
-
Views call methods, not mutate bindings
coordinator.navigateToItem()instead ofselectedItem = item- More explicit, easier to understand
- Understand MVVM first (see MVVM_Architecture.md)
- Identify navigation logic in your ViewModels
- Extract navigation to a Coordinator
- Update views to call Coordinator methods
- Add navigation features (deep linking, etc.)
-
Child Coordinators
- One coordinator per major flow
- Main coordinator coordinates child coordinators
-
Protocol-Based Coordinators
- Define coordination protocols
- Easier to test and mock
-
Coordinator Factory
- Centralize coordinator creation
- Dependency injection
-
Coordinator State Persistence
- Save/restore navigation state
- Handle app termination/restart
- MVVM_Architecture.md - Start here if new to MVVM
- ViewOnly_Architecture.md - SwiftUI basics
- VIPER_Architecture.md - Even more separation
- DATA_FLOW_EXPLANATION.md - How data flows
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.