agentskills.codes
FO

fosmvvm-viewmodel-generator

Generate FOSMVVM ViewModels for SwiftUI screens, pages, and components. Scaffolds RequestableViewModel, localization bindings, and stub factories.

Install

mkdir -p .claude/skills/fosmvvm-viewmodel-generator && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/16383" && unzip -o skill.zip -d .claude/skills/fosmvvm-viewmodel-generator && rm skill.zip

Installs to .claude/skills/fosmvvm-viewmodel-generator

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Generate FOSMVVM ViewModels for SwiftUI screens, pages, and components. Scaffolds RequestableViewModel, localization bindings, and stub factories.
146 charsno explicit “when” trigger

About this skill

FOSMVVM ViewModel Generator

Generate ViewModels following FOSMVVM architecture patterns.

Conceptual Foundation

For full architecture context, see FOSMVVMArchitecture.md | OpenClaw reference

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 @LocalizedString references)
  • 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.

The key question: Where does THIS ViewModel's data come from?

Data SourceHosting ModeFactory
Server/DatabaseServer-HostedHand-written
Local state/preferencesClient-HostedMacro-generated
ResponseError (caught error)Client-HostedMacro-generated

Server-Hosted Mode

When data comes from a server:

  • Factory is hand-written on server (ViewModelFactory protocol)
  • 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, error display

Error Display Pattern

Error display is a classic client-hosted scenario. You already have the data from ResponseError - just wrap it in a specific ViewModel for that error:

// Specific ViewModel for MoveIdeaRequest errors
@ViewModel(options: [.clientHostedFactory])
struct MoveIdeaErrorViewModel {
    let message: LocalizableString
    let errorCode: String

    public var vmId = ViewModelId()

    // Takes the specific ResponseError
    init(responseError: MoveIdeaRequest.ResponseError) {
        self.message = responseError.message
        self.errorCode = responseError.code.rawValue
    }
}

Usage:

catch let error as MoveIdeaRequest.ResponseError {
    let vm = MoveIdeaErrorViewModel(responseError: error)
    return try await req.view.render("Shared/ToastView", vm)
}

Each error scenario gets its own ViewModel:

  • MoveIdeaErrorViewModel for MoveIdeaRequest.ResponseError
  • CreateIdeaErrorViewModel for CreateIdeaRequest.ResponseError
  • SettingsValidationErrorViewModel for settings form errors

Don't create a generic "ToastViewModel" or "ErrorViewModel" - that's unified error architecture, which we avoid.

Key insights:

  • No server request needed - you already caught the error
  • The LocalizableString properties in ResponseError are already localized (server did it)
  • Standard ViewModel → View encoding chain handles this correctly; already-localized strings pass through unchanged
  • Client-hosted ViewModel wraps existing data; the macro generates the factory

Hybrid Apps

Many apps use both:

┌───────────────────────────────────────────────┐
│               iPhone App                       │
├───────────────────────────────────────────────┤
│ SettingsViewModel           → Client-Hosted   │
│ OnboardingViewModel         → Client-Hosted   │
│ MoveIdeaErrorViewModel      → Client-Hosted   │  ← Error display
│ 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:

  1. Factory - what data is needed, how to transform it
  2. 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 TypeHow It's RepresentedExample
Static UI text@LocalizedStringPage titles, button labels (fixed text)
Dynamic enum valuesLocalizableString (stored)Status/state display (see Enum Localization Pattern)
Dynamic data in text@LocalizedSubs"Welcome, %{name}!" with substitutions
Composed text@LocalizedCompoundStringFull name from pieces (locale-aware order)
Formatted datesLocalizableDatecreatedAt: LocalizableDate
Formatted numbersLocalizableInttotalCount: LocalizableInt
Dynamic dataPlain propertiescontent: String, count: Int
Nested componentsChild ViewModelscards: [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 JSON
  • RetrievablePropertyNames - Enables @LocalizedString binding (via @ViewModel macro)
  • Identifiable - Has vmId for SwiftUI identity
  • Stubbable - Has stub() for testing/previews

RequestableViewModel adds:

  • Associated Request type for fetching from server

Two Categories of ViewModels

1. Top-Level (RequestableViewModel)

Represents a full page or screen. Has:

  • An associated ViewModelRequest type
  • A ViewModelFactory that 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:

PurposeViewModel TypeAdopts Fields?
Display data (read-only)Display ViewModelNo
Collect user input (editable)Form ViewModelYes

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

The key question: "Is the user editing data in this ViewModel?"

  • No → Display ViewModel (no Fields)
  • Yes → Form ViewModel (adopt Fields)
ViewModelUser Edits?Adopt Fields?
UserCardViewModelNoNo
`UserRowV

Content truncated.

Search skills

Search the agent skills registry