FOSMVVM SwiftUI View Generator
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-generatorcURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/skilldb%3Afoscomputerservices~fosmvvm-swiftui-view-generator/file -o fosmvvm-swiftui-view-generator.mdGit 仓库获取源码
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