swiftui-component
npx machina-cli add skill jeremieb/swift-unit-test-instructions/swiftui-component --openclawSwiftUI Component Creator
Instructions
Step 1: Clarify Requirements
Ask if not already provided:
- What does the view display? (list, detail, form, modal, etc.)
- What user interactions does it support? (tap, swipe, input, navigation)
- What data does it need? (local state, fetched from API, passed in)
- Any navigation behavior? (NavigationLink, sheet, fullScreenCover)
- Min iOS version? (affects
@ObservablevsObservableObject)
Step 2: Choose the State Pattern
| iOS Target | Pattern |
|---|---|
| iOS 17+ | @Observable macro on ViewModel, @State private var viewModel in View |
| iOS 16 and below | ObservableObject + @Published, @StateObject in View |
Step 3: Generate the Files
Always generate two files: one View, one ViewModel.
View template (iOS 17+ / @Observable):
// Features/[Name]/[Name]View.swift
import SwiftUI
struct [Name]View: View {
@State private var viewModel: [Name]ViewModel
init(viewModel: [Name]ViewModel = [Name]ViewModel()) {
_viewModel = State(initialValue: viewModel)
}
var body: some View {
content
.task { await viewModel.onAppear() }
.alert("Error", isPresented: .constant(viewModel.error != nil)) {
Button("OK") { viewModel.dismissError() }
} message: {
Text(viewModel.error?.localizedDescription ?? "")
}
}
@ViewBuilder
private var content: some View {
if viewModel.isLoading {
ProgressView()
} else {
mainContent
}
}
private var mainContent: some View {
// Primary UI here
Text(viewModel.title)
.accessibilityLabel(viewModel.title)
}
}
#Preview {
[Name]View(viewModel: [Name]ViewModel())
}
View template (iOS 16 / ObservableObject):
// Features/[Name]/[Name]View.swift
import SwiftUI
struct [Name]View: View {
@StateObject private var viewModel: [Name]ViewModel
init(viewModel: [Name]ViewModel = [Name]ViewModel()) {
_viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View {
// same structure as above
}
}
ViewModel template (iOS 17+ / @Observable):
// Features/[Name]/[Name]ViewModel.swift
import Foundation
import Observation
@MainActor
@Observable
final class [Name]ViewModel {
// MARK: - Output State
private(set) var isLoading = false
private(set) var title: String = ""
private(set) var error: Error?
// MARK: - Dependencies
private let service: [Service]Protocol
init(service: [Service]Protocol = [Service]()) {
self.service = service
}
// MARK: - Actions
func onAppear() async {
isLoading = true
defer { isLoading = false }
do {
title = try await service.fetchTitle()
} catch {
self.error = error
}
}
func dismissError() {
error = nil
}
}
ViewModel template (iOS 16 / ObservableObject):
// Features/[Name]/[Name]ViewModel.swift
import Foundation
@MainActor
final class [Name]ViewModel: ObservableObject {
@Published private(set) var isLoading = false
@Published private(set) var title: String = ""
@Published private(set) var error: Error?
private let service: [Service]Protocol
init(service: [Service]Protocol = [Service]()) {
self.service = service
}
func onAppear() async {
isLoading = true
defer { isLoading = false }
do {
title = try await service.fetchTitle()
} catch {
self.error = error
}
}
func dismissError() {
error = nil
}
}
Step 4: Quality Checklist
Before finalizing, verify:
- View has zero business logic — only layout and presentation
- ViewModel has no
import SwiftUI(onlyFoundation,Observation) - All external dependencies are injected via
init -
#Previewcompiles with default / mock data - Loading state is handled
- Error state is handled (alert, inline message, or empty state)
- Interactive elements have
.accessibilityLabelwhere needed - Navigation is handled via ViewModel output flags, not inline logic
Step 5: Sub-View Extraction
If the view body grows complex (>50 lines), extract sub-views:
// Inline private sub-view (same file — preferred for small extractions)
private extension [Name]View {
var headerSection: some View {
VStack(alignment: .leading) {
// ...
}
}
}
For reusable components, create a separate file in Core/Components/.
Examples
Example 1: List screen
User says: "Create a SwiftUI product list screen that fetches products and shows a loading spinner"
Files generated:
ProductListView.swift— list with.task, loading overlay, error alertProductListViewModel.swift— fetches fromProductRepositoryProtocol, manages loading/error state
Example 2: Detail screen
User says: "Build a profile detail screen showing user name, avatar, bio, and a logout button"
Files generated:
ProfileView.swift— avatar (AsyncImage), name, bio, logout buttonProfileViewModel.swift—onLogout()action, user state, logout confirmation flag
Example 3: Form / input screen
User says: "Create a settings screen with a toggle for notifications and a text field for display name"
Files generated:
SettingsView.swift— Form with Toggle and TextField bound to ViewModelSettingsViewModel.swift—notificationsEnabled,displayName,save()async action
Troubleshooting
Preview crashes: ViewModel has a real dependency with side effects. Add a static var preview: [Name]ViewModel with a mock service, or make the default init safe for previews.
View body is growing too large: Extract into private @ViewBuilder computed properties or private extension sub-views. If a sub-view needs significant logic, give it its own ViewModel.
State not updating in view: For @Observable, ensure @State is used in the View (not @StateObject). For ObservableObject, ensure @Published is on the property.
Source
git clone https://github.com/jeremieb/swift-unit-test-instructions/blob/main/skills/swiftui-component/SKILL.mdView on GitHub Overview
Creates SwiftUI views and screens that follow MVVM with thin view layers and a dedicated ViewModel. Generated templates include previews, accessibility support, and built-in loading and error states. It adapts to iOS targets (iOS 17+ using @Observable or iOS 16 with ObservableObject).
How This Skill Works
The tool clarifies requirements, selects a state pattern based on the iOS target, and generates two files: a View and a ViewModel with appropriate scaffolding. It provides templates for both iOS 17+ (@Observable) and iOS 16 (ObservableObject), including onAppear handling, loading indicators, and error presentation for a robust, accessible UI.
When to Use It
- User asks to create a SwiftUI view
- User requests to build a screen
- User wants to add a new view
- User asks to create a component or build the UI for a screen
- User requests adding a SwiftUI screen or making a new SwiftUI page
Quick Start
- Step 1: Clarify Requirements
- Step 2: Choose State Pattern based on iOS target (iOS 17+ vs iOS 16)
- Step 3: Generate the two files (View and ViewModel) using the templates
Best Practices
- Clarify view content, interactions, and data needs before generating templates
- Choose the state pattern based on the target iOS version to ensure compatibility
- Keep the ViewModel thin; extract business logic away from the View
- Leverage previews and accessibility labels in the templates
- Implement loading and error handling consistently to improve UX
Example Use Cases
- Product list screen that shows a loading indicator while fetching items and presents an error alert on failure
- Detail view that fetches and displays a title from a service with a loading state
- Login form with a loading state and error feedback via the ViewModel
- Settings screen with a title sourced from a ViewModel and accessible controls
- Modal sheet presenting a detail view using MVVM and a simple onAppear data fetch