| name | viewmodel |
| description | Creates ViewModels with state management. Use when creating ViewModels, implementing ViewState pattern, or adding state management for features. Delegates to /usecase for domain use cases and to /feature for Container/Feature wiring. |
Skill: ViewModel
Guide for creating ViewModels that manage state and coordinate between Views and UseCases.
Scope & Boundaries
This skill owns only Sources/Presentation/{Screen}/ViewModels/ and its tests.
| Need | Delegate to |
|---|
| Domain UseCases | /usecase skill |
| Container / Feature wiring | /feature skill |
| Navigator / Tracker | /navigator skill |
Workflow
Step 1 — Identify ViewModel Type
Step 2 — Ensure UseCases Exist
Before creating the ViewModel, verify required UseCases exist in Sources/Domain/UseCases/.
- UseCases found? → Go to Step 3
- No UseCases found? → Invoke the
/usecase skill first. Return here after completion.
Step 3 — Implement ViewModel
Read the appropriate reference from Step 1 and implement. Each reference includes: Contract, ViewState (if applicable), ViewModel, View integration snippet, Container factory snippet, Test structure, Mock, and Stub.
- Contract + ViewState + ViewModel in
Sources/Presentation/{Screen}/ViewModels/
- Mock in
Tests/Shared/Mocks/
- Tests in
Tests/Unit/Presentation/{Screen}/ViewModels/
- Run tests
Core Conventions
Class Rules
@Observable only when private(set) var state exists; stateless ViewModels are plain final class
final class, internal visibility, no explicit @MainActor (project default isolation)
- Protocol =
{Screen}ViewModelContract: AnyObject
Method Naming
All protocol methods describe the UI event, using the did prefix:
| UI Event | Protocol Method |
|---|
View appears (.onFirstAppear {}) | didAppear() |
| Tap retry button | didTapOnRetryButton() |
Pull to refresh (.refreshable {}) | didPullToRefresh() |
| Tap load more button | didTapOnLoadMoreButton() |
| Item selection | didSelect(_:) |
| Tap on button | didTapOn{ButtonName}() |
Behavior Rules
didAppear(): Called once via .onFirstAppear — single execution guaranteed by the View
didTapOnRetryButton(): Always calls load() unconditionally
didPullToRefresh(): Always calls the refresh use case, resets pagination
didTapOnLoadMoreButton(): Only loads if there is a next page and not already loading more
- Public methods (
didAppear, didTapOnRetryButton) call private load() — encapsulate loading logic
ViewState == Operator
All ViewState enums implement == for testability (enables direct state comparison in tests). Error cases compare via localizedDescription. See reference files for templates.
File Structure
Features/{Feature}/
├── Sources/Presentation/{Screen}/
│ └── ViewModels/
│ ├── {Screen}ViewModelContract.swift
│ ├── {Screen}ViewState.swift # Only for stateful ViewModels
│ └── {Screen}ViewModel.swift
└── Tests/
├── Unit/Presentation/{Screen}/
│ └── ViewModels/
│ └── {Screen}ViewModelTests.swift
└── Shared/Mocks/
└── {Screen}ViewModelMock.swift
Visibility Summary
| Component | Visibility | Location |
|---|
| ViewModelContract | internal | Sources/Presentation/{Screen}/ViewModels/ |
| ViewState | internal | Sources/Presentation/{Screen}/ViewModels/ |
| ViewModel | internal | Sources/Presentation/{Screen}/ViewModels/ |
| Mock | internal | Tests/Shared/Mocks/ |
Checklist