FOSMVVM SwiftUI View Generator

SkillDB 作者 foscomputerservices v2.0.6

Generate SwiftUI views that render FOSMVVM ViewModels. Scaffolds ViewModelView pattern with binding, loading states, and previews.

源码 ↗

安装 / 下载方式

TotalClaw CLI推荐
totalclaw install skilldb:foscomputerservices~fosmvvm-swiftui-view-generator
cURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/skilldb%3Afoscomputerservices~fosmvvm-swiftui-view-generator/file -o fosmvvm-swiftui-view-generator.md
Git 仓库获取源码
git clone https://github.com/openclaw/skills/commit/25f57a8278a5b61e3f9b869145368b7118c30415
# FOSMVVM SwiftUI View Generator

Generate SwiftUI views that render FOSMVVM ViewModels.

## Conceptual Foundation

> For full architecture context, see [FOSMVVMArchitecture.md](../../docs/FOSMVVMArchitecture.md) | [OpenClaw reference]({baseDir}/references/FOSMVVMArchitecture.md)

In FOSMVVM, **Views are thin rendering layers** that display ViewModels:

```
┌─────────────────────────────────────────────────────────────┐
│                    ViewModelView Pattern                     │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ViewModel (Data)          ViewModelView (SwiftUI)          │
│  ┌──────────────────┐     ┌──────────────────┐             │
│  │ title: String    │────►│ Text(vm.title)   │             │
│  │ items: [Item]    │────►│ ForEach(vm.items)│             │
│  │ isEnabled: Bool  │────►│ .disabled(!...)  │             │
│  └──────────────────┘     └──────────────────┘             │
│                                                              │
│  Operations (Actions)                                        │
│  ┌──────────────────┐     ┌──────────────────┐             │
│  │ submit()         │◄────│ Button(action:)  │             │
│  │ cancel()         │◄────│ .onAppear { }    │             │
│  └──────────────────┘     └──────────────────┘             │
│                                                              │
└─────────────────────────────────────────────────────────────┘
```

**Key principle:** Views don't transform or compute data. They render what the ViewModel provides.

---

## View-ViewModel Alignment

**The View filename should match the ViewModel it renders.**

```
Sources/
  {ViewModelsTarget}/
    {Feature}/
      {Feature}ViewModel.swift        ←──┐
      {Entity}CardViewModel.swift     ←──┼── Same names
                                          │
  {ViewsTarget}/                          │
    {Feature}/                            │
      {Feature}View.swift             ────┤  (renders {Feature}ViewModel)
      {Entity}CardView.swift          ────┘  (renders {Entity}CardViewModel)
```

This alignment provides:
- **Discoverability** - Find the view for any ViewModel instantly
- **Consistency** - Same naming discipline across the codebase
- **Maintainability** - Changes to ViewModel are reflected in view location

---

## Core Components

### 1. ViewModelView Protocol

Every view conforms to `ViewModelView`:

```swift
public struct MyView: ViewModelView {
    private let viewModel: MyViewModel

    public var body: some View {
        Text(viewModel.title)
    }

    public init(viewModel: MyViewModel) {
        self.viewModel = viewModel
    }
}
```

**Required:**
- `private let viewModel: {ViewModel}`
- `public init(viewModel:)`
- Conforms to `ViewModelView` protocol

### 2. Operations (Optional)

Interactive views have operations:

```swift
public struct MyView: ViewModelView {
    private let viewModel: MyViewModel
    private let operations: MyViewModelOperations

    #if DEBUG
    @State private var repaintToggle = false
    #endif

    public var body: some View {
        Button(action: performAction) {
            Text(viewModel.buttonLabel)
        }
        #if DEBUG
        .testDataTransporter(viewModelOps: operations, repaintToggle: $repaintToggle)
        #endif
    }

    public init(viewModel: MyViewModel) {
        self.viewModel = viewModel
        self.operations = viewModel.operations
    }

    private func performAction() {
        operations.performAction()
        toggleRepaint()
    }

    private func toggleRepaint() {
        #if DEBUG
        repaintToggle.toggle()
        #endif
    }
}
```

**When views have operations:**
- Store `operations` from `viewModel.operations` in init
- Add `@State private var repaintToggle = false` (DEBUG only)
- Add `.testDataTransporter(viewModelOps:repaintToggle:)` modifier (DEBUG only)
- Call `toggleRepaint()` after every operation invocation

### 3. Child View Binding

Parent views bind child views using `.bind(appState:)`:

```swift
public struct ParentView: ViewModelView {
    @Environment(AppState.self) private var appState
    private let viewModel: ParentViewModel

    public var body: some View {
        VStack {
            Text(viewModel.title)

            // Bind child view with subset of parent's data
            ChildView.bind(
                appState: .init(
                    itemId: viewModel.selectedId,
                    isConnected: viewModel.isConnected
                )
            )
        }
    }
}
```

**The `.bind()` pattern:**
- Child views use `.bind(appState:)` to receive data from parent
- Parent creates child's `AppState` from its own ViewModel data
- Enables composition without tight coupling

### 4. Form Views with Validation

Forms use `FormFieldView` and `Validations` environment:

```swift
public struct MyFormView: ViewModelView {
    @Environment(Validations.self) private var validations
    @Environment(\.focusState) private var focusField
    @State private var error: Error?

    private let viewModel: MyFormViewModel
    private let operations: MyFormViewModelOperations

    public var body: some View {
        Form {
            FormFieldView(
                fieldModel: viewModel.$email,
                focusField: focusField,
                fieldValidator: viewModel.validateEmail,
                validations: validations
            )

            Button(errorBinding: $error, asyncAction: submit) {
                Text(viewModel.submitButtonLabel)
            }
            .disabled(validations.hasError)
        }
        .onAsyncSubmit {
            await submit()
        }
        .alert(
            errorBinding: $error,
            title: viewModel.errorTitle,
            message: viewModel.errorMessage,
            dismissButtonLabel: viewModel.dismissButtonLabel
        )
    }
}
```

**Form patterns:**
- `@Environment(Validations.self)` for validation state
- `FormFieldView` for each input field
- `Button(errorBinding:asyncAction:)` for async actions
- `.disabled(validations.hasError)` on submit button
- Separate handling for validation errors vs general errors

### 5. Previews

Use `.previewHost()` for SwiftUI previews:

```swift
#if DEBUG
#Preview {
    MyView.previewHost(
        bundle: MyAppResourceAccess.localizationBundle
    )
    .environment(AppState())
}

#Preview("With Data") {
    MyView.previewHost(
        bundle: MyAppResourceAccess.localizationBundle,
        viewModel: .stub(title: "Preview Title")
    )
    .environment(AppState())
}
#endif
```

## View Categories

### Display-Only Views

Views that just render data (no user interactions):

```swift
public struct InfoView: ViewModelView {
    private let viewModel: InfoViewModel

    public var body: some View {
        VStack {
            Text(viewModel.title)
            Text(viewModel.description)

            if viewModel.isActive {
                Text(viewModel.activeStatusLabel)
            }
        }
    }

    public init(viewModel: InfoViewModel) {
        self.viewModel = viewModel
    }
}
```

**Characteristics:**
- No `operations` property
- No `repaintToggle` or `testDataTransporter`
- Just renders ViewModel properties
- May have conditional rendering based on ViewModel state

### Interactive Views

Views with user actions:

```swift
public struct ActionView: ViewModelView {
    @State private var error: Error?

    private let viewModel: ActionViewModel
    private let operations: ActionViewModelOperations

    #if DEBUG
    @State private var repaintToggle = false
    #endif

    public var body: some View {
        VStack {
            Button(action: performAction) {
                Text(viewModel.actionLabel)
            }

            Button(role: .cancel, action: cancel) {
                Text(viewModel.cancelLabel)
            }
        }
        .alert(
            errorBinding: $error,
            title: viewModel.errorT