่ทณ่‡ณไธป่ฆๅ†…ๅฎน
ๅฐ้พ™่™พๅฐ้พ™่™พAI
๐Ÿค–

FOSMVVM ViewModel Generator

Generate FOSMVVM-compliant SwiftUI ViewModels with client- or server-hosted factories, localization bindings, and stub factories for screens and components.

ไธ‹่ฝฝ350
ๆ˜Ÿๆ ‡2
็‰ˆๆœฌ2.0.6
ๅผ€ๅ‘ๅทฅๅ…ท
ๅฎ‰ๅ…จ้€š่ฟ‡
๐Ÿ’ฌPrompt

ๆŠ€่ƒฝ่ฏดๆ˜Ž


name: fosmvvm-viewmodel-generator description: Generate FOSMVVM ViewModels for SwiftUI screens, pages, and components. Scaffolds RequestableViewModel, localization bindings, and stub factories. homepage: https://github.com/foscomputerservices/FOSUtilities metadata: {"clawdbot": {"emoji": "๐Ÿ—๏ธ", "os": ["darwin", "linux"]}}

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
UserRowViewModelNoNo
UserDetailViewModelNoNo
UserFormViewModelYesUserFields
CreateUserViewModelYesUserFields
EditUserViewModelYesUserFields
SettingsViewModelYesSettingsFields

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)

FileLocationPurpose
{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)

FileLocationPurpose
{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)

FileLocationPurpose
{Name}ViewModel.swift{ViewModelsTarget}/The ViewModel struct
{Name}ViewModel.yml{ResourcesPath}/Localization (if has @LocalizedString)

Note: If child is only used by one parent and represents a summary/reference (not a full ViewModel), nest it inside the parent file instead. See Nested Child Types Pattern under Key Patterns.

Project Structure Configuration

PlaceholderDescriptionExample
{ViewModelsTarget}Shared ViewModels SPM targetViewModels
{ResourcesPath}Localization resourcesSources/Resources
{WebServerTarget}Server-side targetWebServer, AppServer

How to Use This Skill

Invocation: /fosmvvm-viewmodel-generator

Prerequisites:

  • View requirements understood from conversation context
  • Data source determined (server/database vs local state)
  • Display vs Form decision made (if user input involved, Fields protocol exists)

Workflow integration: This skill is typically used after discussing View requirements or reading specification files. The skill references conversation context automaticallyโ€”no file paths or Q&A needed. For Form ViewModels, run fosmvvm-fields-generator first to create the Fields protocol.

Pattern Implementation

This skill references conversation context to determine ViewModel structure:

Hosting Mode Detection

From conversation context, the skill identifies:

  • Data source (server/database vs local state/preferences)
  • Server-hosted โ†’ Hand-written factory, server-side localization
  • Client-hosted โ†’ Macro-generated factory, client-side localization

ViewModel Design

From requirements already in context:

  • View purpose (page, modal, card, row component)
  • Data needs (from database query, from AppState, from caught error)
  • Static UI text (titles, labels, buttons requiring @LocalizedString)
  • Child ViewModels (nested components)
  • Hierarchy level (top-level RequestableViewModel vs child ViewModel)

Property Planning

Based on View requirements:

  • Display properties (data to render)
  • Localization requirements (which properties use @LocalizedString)
  • Identity strategy (singleton vmId vs instance-based vmId)
  • Form adoption (whether ViewModel adopts Fields protocol)

File Generation

Server-Hosted Top-Level:

  1. ViewModel struct (with RequestableViewModel)
  2. Request type
  3. YAML localization
  4. Factory implementation

Client-Hosted Top-Level:

  1. ViewModel struct (with clientHostedFactory option)
  2. YAML localization

Child (either mode):

  1. ViewModel struct
  2. YAML localization (if needed)

Context Sources

Skill references information from:

  • Prior conversation: View requirements, data sources discussed with user
  • Specification files: If Claude has read UI specs or feature docs into context
  • Fields protocols: From codebase or previous fosmvvm-fields-generator invocation

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.

Enum Localization Pattern

For dynamic enum values (status, state, category), use a stored LocalizableString - NOT @LocalizedString.

@LocalizedString always looks up the same key (the property name). A stored LocalizableString carries the dynamic key from the enum case.

// Enum provides localizableString
public enum SessionState: String, CaseIterable, Codable, Sendable {
    case pending, running, completed, failed

    public var localizableString: LocalizableString {
        .localized(for: Self.self, propertyName: rawValue)
    }
}

// ViewModel stores it (NOT @LocalizedString)
@ViewModel
public struct SessionCardViewModel {
    public let state: SessionState                // Raw enum for data attributes
    public let stateDisplay: LocalizableString   // Localized display text

    public init(session: Session) {
        self.state = session.state
        self.stateDisplay = session.state.localizableString
    }
}
# YAML keys match enum type and case names
en:
  SessionState:
    pending: "Pending"
    running: "Running"
    completed: "Completed"
    failed: "Failed"

Constraint: LocalizableString only works in ViewModels encoded with localizingEncoder(). Do not use in Fluent JSONB fields or other persisted types.

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.

Nested Child Types Pattern

When a child type is only used by one parent and represents a summary or reference (not a full ViewModel), nest it inside the parent:

@ViewModel
public struct GovernancePrincipleCardViewModel: Codable, Sendable, Identifiable {
    // Properties come first
    public let versionHistory: [GovernancePrincipleVersionSummary]?
    public let referencingDecisions: [GovernanceDecisionReference]?

    // MARK: - Nested Types

    /// Summary of a principle version for display in version history.
    public struct GovernancePrincipleVersionSummary: Codable, Sendable, Identifiable, Stubbable {
        public let id: ModelIdType
        public let version: Int
        public let createdAt: Date

        public init(id: ModelIdType, version: Int, createdAt: Date) {
            self.id = id
            self.version = version
            self.createdAt = createdAt
        }
    }

    /// Reference to a decision that cites this principle.
    public struct GovernanceDecisionReference: Codable, Sendable, Identifiable, Stubbable {
        public let id: ModelIdType
        public let title: String
        public let decisionNumber: String
        public let createdAt: Date

        public init(id: ModelIdType, title: String, decisionNumber: String, createdAt: Date) {
            self.id = id
            self.title = title
            self.decisionNumber = decisionNumber
            self.createdAt = createdAt
        }
    }

    // vmId and parent init follow
    public let vmId: ViewModelId
    // ...
}

Reference: Sources/KairosModels/Governance/GovernancePrincipleCardViewModel.swift

Placement rules:

  1. Nested types go AFTER the properties that reference them
  2. Before vmId and the parent's init
  3. Use // MARK: - Nested Types section marker
  4. Each nested type gets its own doc comment

Conformances for nested types:

  • Codable - for ViewModel encoding
  • Sendable - for Swift 6 concurrency
  • Identifiable - for SwiftUI ForEach if used in arrays
  • Stubbable - for testing/previews

Two-Tier Stubbable Pattern:

Nested types use fully qualified names in their extensions:

public extension GovernancePrincipleCardViewModel.GovernancePrincipleVersionSummary {
    // Tier 1: Zero-arg convenience (ALWAYS delegates to tier 2)
    static func stub() -> Self {
        .stub(id: .init())
    }

    // Tier 2: Full parameterized with defaults
    static func stub(
        id: ModelIdType = .init(),
        version: Int = 1,
        createdAt: Date = .now
    ) -> Self {
        .init(id: id, version: version, createdAt: createdAt)
    }
}

public extension GovernancePrincipleCardViewModel.GovernanceDecisionReference {
    static func stub() -> Self {
        .stub(id: .init())
    }

    static func stub(
        id: ModelIdType = .init(),
        title: String = "A Title",
        decisionNumber: String = "DEC-12345",
        createdAt: Date = .now
    ) -> Self {
        .init(id: id, title: title, decisionNumber: decisionNumber, createdAt: createdAt)
    }
}

Why two tiers:

  • Tests often just need [.stub()] without caring about values
  • Other tests need specific values: .stub(name: "Specific Name")
  • Zero-arg ALWAYS calls parameterized version (single source of truth)

When to nest vs keep top-level:

Nest Inside ParentKeep Top-Level
Child is ONLY used by this parentChild is shared across multiple parents
Child represents subset/summaryChild is a full ViewModel
Child has no @ViewModel macroChild has @ViewModel macro
Child is not RequestableViewModelChild is RequestableViewModel
Example: VersionSummary, ReferenceExample: CardViewModel, ListViewModel

Examples:

Card with nested summaries:

@ViewModel
public struct TaskCardViewModel {
    public let assignees: [AssigneeSummary]?

    public struct AssigneeSummary: Codable, Sendable, Identifiable, Stubbable {
        public let id: ModelIdType
        public let name: String
        public let avatarUrl: String?
        // ...
    }
}

List with nested references:

@ViewModel
public struct ProjectListViewModel {
    public let relatedProjects: [ProjectReference]?

    public struct ProjectReference: Codable, Sendable, Identifiable, Stubbable {
        public let id: ModelIdType
        public let title: String
        public let status: String
        // ...
    }
}

Codable and Computed Properties

Swift's synthesized Codable only encodes stored properties. Since ViewModels are serialized (for JSON transport, Leaf rendering, etc.), computed properties won't be available.

// Computed - NOT encoded, invisible after serialization
public var hasCards: Bool { !cards.isEmpty }

// Stored - encoded, available after serialization
public let hasCards: Bool

When to pre-compute:

For Leaf templates, you can often use Leaf's built-in functions directly:

  • #if(count(cards) > 0) - no need for hasCards property
  • #count(cards) - no need for cardCount property

Pre-compute only when:

  • Direct array subscripts needed (firstCard - array indexing not documented in Leaf)
  • Complex logic that's cleaner in Swift than in template
  • Performance-sensitive repeated calculations

See fosmvvm-leaf-view-generator for Leaf template patterns.

File Templates

See reference.md for complete file templates.

Naming Conventions

ConceptConventionExample
ViewModel struct{Name}ViewModelDashboardViewModel
Request class{Name}RequestDashboardRequest
Factory extension{Name}ViewModel+Factory.swiftDashboardViewModel+Factory.swift
YAML file{Name}ViewModel.ymlDashboardViewModel.yml

See Also

Version History

VersionDateChanges
1.02024-12-24Initial skill
2.02024-12-26Complete rewrite from architecture; generalized from Kairos-specific
2.12024-12-26Added Client-Hosted mode support; per-ViewModel hosting decision
2.22024-12-26Added shaping responsibility, @LocalizedSubs/@LocalizedCompoundString, anti-pattern
2.32025-12-27Added Display vs Form ViewModels section; clarified Fields adoption
2.42026-01-08Added Codable/computed properties section. Clarified when to pre-compute vs use Leaf built-ins.
2.52026-01-19Added Enum Localization Pattern section. Clarified @LocalizedString is for static text only; stored LocalizableString for dynamic enum values.
2.62026-01-24Update to context-aware approach (remove file-parsing/Q&A). Skill references conversation context instead of asking questions or accepting file paths.
2.72026-01-25Added Nested Child Types Pattern section with two-tier Stubbable pattern, placement rules, conformances, and decision criteria for when to nest vs keep top-level.

ๅฆ‚ไฝ•ไฝฟ็”จใ€ŒFOSMVVM ViewModel Generatorใ€๏ผŸ

  1. ๆ‰“ๅผ€ๅฐ้พ™่™พAI๏ผˆWeb ๆˆ– iOS App๏ผ‰
  2. ็‚นๅ‡ปไธŠๆ–นใ€Œ็ซ‹ๅณไฝฟ็”จใ€ๆŒ‰้’ฎ๏ผŒๆˆ–ๅœจๅฏน่ฏๆก†ไธญ่พ“ๅ…ฅไปปๅŠกๆ่ฟฐ
  3. ๅฐ้พ™่™พAI ไผš่‡ชๅŠจๅŒน้…ๅนถ่ฐƒ็”จใ€ŒFOSMVVM ViewModel Generatorใ€ๆŠ€่ƒฝๅฎŒๆˆไปปๅŠก
  4. ็ป“ๆžœๅณๆ—ถๅ‘ˆ็Žฐ๏ผŒๆ”ฏๆŒ็ปง็ปญๅฏน่ฏไผ˜ๅŒ–

็›ธๅ…ณๆŠ€่ƒฝ