| name | fosmvvm-viewmodel-generator |
| description | Generate FOSMVVM ViewModels - the bridge between server-side data and client-side Views. Use when creating new screens, pages, components, or any UI that displays data. |
FOSMVVM ViewModel Generator
Generate ViewModels following FOSMVVM architecture patterns.
Conceptual Foundation
For full architecture context, see FOSMVVMArchitecture.md
A ViewModel is the bridge in the Model-View-ViewModel architecture:
┌─────────────┐ ┌─────────────────┐ ┌─────────────┐
│ Model │ ───► │ ViewModel │ ───► │ View │
│ (Data) │ │ (The Bridge) │ │ (SwiftUI) │
└─────────────┘ └─────────────────┘ └─────────────┘
Key insight: In FOSMVVM, ViewModels are:
- Created by a Factory (either server-side or client-side)
- Localized during encoding (resolves all
@LocalizedStringreferences) - Consumed by Views which just render the localized data
First Decision: Hosting Mode
This is a per-ViewModel decision. An app can mix both modes - for example, a standalone iPhone app with server-based sign-in.
Ask: Where does THIS ViewModel's data come from?
| Data Source | Hosting Mode | Factory |
|---|---|---|
| Server/Database | Server-Hosted | Hand-written |
| Local state/preferences | Client-Hosted | Macro-generated |
Server-Hosted Mode
When data comes from a server:
- Factory is hand-written on server (
ViewModelFactoryprotocol) - Factory queries database, builds ViewModel
- Server localizes during JSON encoding
- Client receives fully localized ViewModel
Examples: Sign-in screen, user profile from API, dashboard with server data
Client-Hosted Mode
When data is local to the device:
- Use
@ViewModel(options: [.clientHostedFactory]) - Macro auto-generates factory from init parameters
- Client bundles YAML resources
- Client localizes during encoding
Examples: Settings screen, onboarding, offline-first features
Hybrid Apps
Many apps use both:
┌─────────────────────────────────────────┐
│ iPhone App │
├─────────────────────────────────────────┤
│ SettingsViewModel → Client-Hosted │
│ OnboardingViewModel → Client-Hosted │
│ SignInViewModel → Server-Hosted │
│ UserProfileViewModel → Server-Hosted │
└─────────────────────────────────────────┘
Same ViewModel patterns work in both modes - only the factory creation differs.
Core Responsibility: Shaping Data
A ViewModel's job is shaping data for presentation. This happens in two places:
- Factory - what data is needed, how to transform it
- Localization - how to present it in context (including locale-aware ordering)
The View just renders - it should never compose, format, or reorder ViewModel properties.
What a ViewModel Contains
A ViewModel answers: "What does the View need to display?"
| Content Type | How It's Represented | Example |
|---|---|---|
| Static UI text | @LocalizedString |
Page titles, button labels |
| Dynamic data in text | @LocalizedSubs |
"Welcome, %{name}!" with substitutions |
| Composed text | @LocalizedCompoundString |
Full name from pieces (locale-aware order) |
| Formatted dates | LocalizableDate |
createdAt: LocalizableDate |
| Formatted numbers | LocalizableInt |
totalCount: LocalizableInt |
| Dynamic data | Plain properties | content: String, count: Int |
| Nested components | Child ViewModels | cards: [CardViewModel] |
What a ViewModel Does NOT Contain
- Database relationships (
@Parent,@Siblings) - Business logic or validation (that's in Fields protocols)
- Raw database IDs exposed to templates (use typed properties)
- Unlocalized strings that Views must look up
Anti-Pattern: Composition in Views
// ❌ WRONG - View is composing
Text(viewModel.firstName) + Text(" ") + Text(viewModel.lastName)
// ✅ RIGHT - ViewModel provides shaped result
Text(viewModel.fullName) // via @LocalizedCompoundString
If you see + or string interpolation in a View, the shaping belongs in the ViewModel.
ViewModel Protocol Hierarchy
public protocol ViewModel: ServerRequestBody, RetrievablePropertyNames, Identifiable, Stubbable {
var vmId: ViewModelId { get }
}
public protocol RequestableViewModel: ViewModel {
associatedtype Request: ViewModelRequest
}
ViewModel provides:
ServerRequestBody- Can be sent over HTTP as JSONRetrievablePropertyNames- Enables@LocalizedStringbinding (via@ViewModelmacro)Identifiable- HasvmIdfor SwiftUI identityStubbable- Hasstub()for testing/previews
RequestableViewModel adds:
- Associated
Requesttype for fetching from server
Two Categories of ViewModels
1. Top-Level (RequestableViewModel)
Represents a full page or screen. Has:
- An associated
ViewModelRequesttype - A
ViewModelFactorythat builds it from database - Child ViewModels embedded within it
@ViewModel
public struct DashboardViewModel: RequestableViewModel {
public typealias Request = DashboardRequest
@LocalizedString public var pageTitle
public let cards: [CardViewModel] // Children
public var vmId: ViewModelId = .init()
}
2. Child (plain ViewModel)
Nested components built by their parent's factory. No Request type.
@ViewModel
public struct CardViewModel: Codable, Sendable {
public let id: ModelIdType
public let title: String
public let createdAt: LocalizableDate
public var vmId: ViewModelId = .init()
}
Display vs Form ViewModels
ViewModels serve two distinct purposes:
| Purpose | ViewModel Type | Adopts Fields? |
|---|---|---|
| Display data (read-only) | Display ViewModel | No |
| Collect user input (editable) | Form ViewModel | Yes |
Display ViewModels
For showing data - cards, rows, lists, detail views:
@ViewModel
public struct UserCardViewModel {
public let id: ModelIdType
public let name: String
@LocalizedString public var roleDisplayName
public let createdAt: LocalizableDate
public var vmId: ViewModelId = .init()
}
Characteristics:
- Properties are
let(read-only) - No validation needed
- No FormField definitions
- Just projects Model data for display
Form ViewModels
For collecting input - create forms, edit forms, settings:
@ViewModel
public struct UserFormViewModel: UserFields { // ← Adopts Fields!
public var id: ModelIdType?
public var email: String
public var firstName: String
public var lastName: String
public let userValidationMessages: UserFieldsMessages
public var vmId: ViewModelId = .init()
}
Characteristics:
- Properties are
var(editable) - Adopts a Fields protocol for validation
- Gets FormField definitions from Fields
- Gets validation logic from Fields
- Gets localized error messages from Fields
The Connection
┌─────────────────────────────────────────────────────────────────┐
│ UserFields Protocol │
│ (defines editable properties + validation) │
│ │
│ Adopted by: │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ CreateUserReq │ │ UserFormVM │ │ User (Model) │ │
│ │ .RequestBody │ │ (UI form) │ │ (persistence) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
│ Same validation logic everywhere! │
└─────────────────────────────────────────────────────────────────┘
Quick Decision Guide
Ask: "Is the user editing data in this ViewModel?"
- No → Display ViewModel (no Fields)
- Yes → Form ViewModel (adopt Fields)
| ViewModel | User Edits? | Adopt Fields? |
|---|---|---|
UserCardViewModel |
No | No |
UserRowViewModel |
No | No |
UserDetailViewModel |
No | No |
UserFormViewModel |
Yes | UserFields |
CreateUserViewModel |
Yes | UserFields |
EditUserViewModel |
Yes | UserFields |
SettingsViewModel |
Yes | SettingsFields |
When to Use This Skill
- Creating a new page or screen
- Adding a new UI component (card, row, modal, etc.)
- Displaying data from the database in a View
- Following an implementation plan that requires new ViewModels
What This Skill Generates
Server-Hosted: Top-Level ViewModel (4 files)
| File | Location | Purpose |
|---|---|---|
{Name}ViewModel.swift |
{ViewModelsTarget}/ |
The ViewModel struct |
{Name}Request.swift |
{ViewModelsTarget}/ |
The ViewModelRequest type |
{Name}ViewModel.yml |
{ResourcesPath}/ |
Localization strings |
{Name}ViewModel+Factory.swift |
{WebServerTarget}/ |
Factory that builds from DB |
Client-Hosted: Top-Level ViewModel (2 files)
| File | Location | Purpose |
|---|---|---|
{Name}ViewModel.swift |
{ViewModelsTarget}/ |
ViewModel with clientHostedFactory option |
{Name}ViewModel.yml |
{ResourcesPath}/ |
Localization strings (bundled in app) |
No Request or Factory files needed - macro generates them!
Child ViewModels (1-2 files, either mode)
| File | Location | Purpose |
|---|---|---|
{Name}ViewModel.swift |
{ViewModelsTarget}/ |
The ViewModel struct |
{Name}ViewModel.yml |
{ResourcesPath}/ |
Localization (if has @LocalizedString) |
Project Structure Configuration
| Placeholder | Description | Example |
|---|---|---|
{ViewModelsTarget} |
Shared ViewModels SPM target | ViewModels |
{ResourcesPath} |
Localization resources | Sources/Resources |
{WebServerTarget} |
Server-side target | WebServer, AppServer |
Generation Process
Step 1: Determine Hosting Mode
Ask: Where does this ViewModel's data come from?
- Server/database → Server-Hosted (hand-written factory)
- Local state → Client-Hosted (macro-generated factory)
Step 2: Understand What the View Needs
Clarify:
- What is this View displaying? (page, modal, card, row?)
- What data does it need? (from database? from AppState?)
- What static text does it have? (titles, labels, buttons)
- Does it contain child ViewModels?
- Is it top-level or a child?
Step 3: Design the ViewModel
Determine:
- Properties - What does the View need to render?
- Localization - Which properties are
@LocalizedString? - Identity - Singleton (
vmId = .init(type: Self.self)) or instance (vmId = .init(id: id))?
Step 4: Generate Files
Server-Hosted Top-Level:
- ViewModel struct (with
RequestableViewModel) - Request type
- YAML localization
- Factory implementation
Client-Hosted Top-Level:
- ViewModel struct (with
clientHostedFactoryoption) - YAML localization
Child (either mode):
- ViewModel struct
- YAML localization (if needed)
Key Patterns
The @ViewModel Macro
Always use the @ViewModel macro - it generates the propertyNames() method required for localization binding.
Server-Hosted (basic macro):
@ViewModel
public struct MyViewModel: RequestableViewModel {
public typealias Request = MyRequest
@LocalizedString public var title
public var vmId: ViewModelId = .init()
public init() {}
}
Client-Hosted (with factory generation):
@ViewModel(options: [.clientHostedFactory])
public struct SettingsViewModel {
@LocalizedString public var pageTitle
public var vmId: ViewModelId = .init()
public init(theme: Theme, notifications: NotificationSettings) {
// Init parameters become AppState properties
}
}
// Macro auto-generates:
// - typealias Request = ClientHostedRequest
// - struct AppState { let theme: Theme; let notifications: NotificationSettings }
// - class ClientHostedRequest: ViewModelRequest { ... }
// - static func model(context:) async throws -> Self { ... }
Stubbable Pattern
All ViewModels must support stub() for testing and SwiftUI previews:
public extension MyViewModel {
static func stub() -> Self {
.init(/* default values */)
}
}
Identity: vmId
Every ViewModel needs a vmId for SwiftUI's identity system:
Singleton (one per page): vmId = .init(type: Self.self)
Instance (multiple per page): vmId = .init(id: id) where id: ModelIdType
Localization
Static UI text uses @LocalizedString:
@LocalizedString public var pageTitle
With corresponding YAML:
en:
MyViewModel:
pageTitle: "Welcome"
Dates and Numbers
Never send pre-formatted strings. Use localizable types:
public let createdAt: LocalizableDate // NOT String
public let itemCount: LocalizableInt // NOT String
The client formats these according to user's locale and timezone.
Child ViewModels
Top-level ViewModels contain their children:
@ViewModel
public struct BoardViewModel: RequestableViewModel {
public let columns: [ColumnViewModel]
public let cards: [CardViewModel]
}
The Factory builds all children when building the parent.
File Templates
See reference.md for complete file templates.
Naming Conventions
| Concept | Convention | Example |
|---|---|---|
| ViewModel struct | {Name}ViewModel |
DashboardViewModel |
| Request class | {Name}Request |
DashboardRequest |
| Factory extension | {Name}ViewModel+Factory.swift |
DashboardViewModel+Factory.swift |
| YAML file | {Name}ViewModel.yml |
DashboardViewModel.yml |
Collaboration Protocol
- Understand what the View needs to display
- Confirm whether it's top-level or child
- Display or Form? - If form, use fields-generator first to create Fields protocol
- Identify which properties need localization
- Generate files one at a time with feedback
See Also
- FOSMVVMArchitecture.md - Full FOSMVVM architecture
- fosmvvm-fields-generator - For form validation
- fosmvvm-fluent-datamodel-generator - For Fluent persistence layer
- fosmvvm-leaf-view-generator - For Leaf templates that render ViewModels
- reference.md - Complete file templates
Version History
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2024-12-24 | Initial skill |
| 2.0 | 2024-12-26 | Complete rewrite from architecture; generalized from Kairos-specific |
| 2.1 | 2024-12-26 | Added Client-Hosted mode support; per-ViewModel hosting decision |
| 2.2 | 2024-12-26 | Added shaping responsibility, @LocalizedSubs/@LocalizedCompoundString, anti-pattern |
| 2.3 | 2025-12-27 | Added Display vs Form ViewModels section; clarified Fields adoption |