mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 21:53:28 +08:00
feat: add swiftui-patterns skill
Add comprehensive SwiftUI skill covering @Observable state management, view composition, type-safe NavigationStack routing, performance optimization with lazy containers, and modern preview patterns.
This commit is contained in:
257
skills/swiftui-patterns/SKILL.md
Normal file
257
skills/swiftui-patterns/SKILL.md
Normal file
@@ -0,0 +1,257 @@
|
||||
---
|
||||
name: swiftui-patterns
|
||||
description: SwiftUI architecture patterns, state management with @Observable, view composition, navigation, performance optimization, and modern iOS/macOS UI best practices.
|
||||
---
|
||||
|
||||
# SwiftUI Patterns
|
||||
|
||||
Modern SwiftUI patterns for building declarative, performant user interfaces on Apple platforms. Covers the Observation framework, view composition, type-safe navigation, and performance optimization.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Building SwiftUI views and managing state (`@State`, `@Observable`, `@Binding`)
|
||||
- Designing navigation flows with `NavigationStack`
|
||||
- Structuring view models and data flow
|
||||
- Optimizing rendering performance for lists and complex layouts
|
||||
- Working with environment values and dependency injection in SwiftUI
|
||||
|
||||
## State Management
|
||||
|
||||
### Property Wrapper Selection
|
||||
|
||||
Choose the simplest wrapper that fits:
|
||||
|
||||
| Wrapper | Use Case |
|
||||
|---------|----------|
|
||||
| `@State` | View-local value types (toggles, form fields, sheet presentation) |
|
||||
| `@Binding` | Two-way reference to parent's `@State` |
|
||||
| `@Observable` class + `@State` | Owned model with multiple properties |
|
||||
| `@Observable` class (no wrapper) | Read-only reference passed from parent |
|
||||
| `@Bindable` | Two-way binding to an `@Observable` property |
|
||||
| `@Environment` | Shared dependencies injected via `.environment()` |
|
||||
|
||||
### @Observable ViewModel
|
||||
|
||||
Use `@Observable` (not `ObservableObject`) — it tracks property-level changes so SwiftUI only re-renders views that read the changed property:
|
||||
|
||||
```swift
|
||||
@Observable
|
||||
final class ItemListViewModel {
|
||||
private(set) var items: [Item] = []
|
||||
private(set) var isLoading = false
|
||||
var searchText = ""
|
||||
|
||||
private let repository: any ItemRepository
|
||||
|
||||
init(repository: any ItemRepository = DefaultItemRepository()) {
|
||||
self.repository = repository
|
||||
}
|
||||
|
||||
func load() async {
|
||||
isLoading = true
|
||||
defer { isLoading = false }
|
||||
items = (try? await repository.fetchAll()) ?? []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### View Consuming the ViewModel
|
||||
|
||||
```swift
|
||||
struct ItemListView: View {
|
||||
@State private var viewModel = ItemListViewModel()
|
||||
|
||||
var body: some View {
|
||||
List(viewModel.items) { item in
|
||||
ItemRow(item: item)
|
||||
}
|
||||
.searchable(text: $viewModel.searchText)
|
||||
.overlay { if viewModel.isLoading { ProgressView() } }
|
||||
.task { await viewModel.load() }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Injection
|
||||
|
||||
Replace `@EnvironmentObject` with `@Environment`:
|
||||
|
||||
```swift
|
||||
// Inject
|
||||
ContentView()
|
||||
.environment(authManager)
|
||||
|
||||
// Consume
|
||||
struct ProfileView: View {
|
||||
@Environment(AuthManager.self) private var auth
|
||||
|
||||
var body: some View {
|
||||
Text(auth.currentUser?.name ?? "Guest")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## View Composition
|
||||
|
||||
### Extract Subviews to Limit Invalidation
|
||||
|
||||
Break views into small, focused structs. When state changes, only the subview reading that state re-renders:
|
||||
|
||||
```swift
|
||||
struct OrderView: View {
|
||||
@State private var viewModel = OrderViewModel()
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
OrderHeader(title: viewModel.title)
|
||||
OrderItemList(items: viewModel.items)
|
||||
OrderTotal(total: viewModel.total)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ViewModifier for Reusable Styling
|
||||
|
||||
```swift
|
||||
struct CardModifier: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.padding()
|
||||
.background(.regularMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func cardStyle() -> some View {
|
||||
modifier(CardModifier())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Navigation
|
||||
|
||||
### Type-Safe NavigationStack
|
||||
|
||||
Use `NavigationStack` with `NavigationPath` for programmatic, type-safe routing:
|
||||
|
||||
```swift
|
||||
@Observable
|
||||
final class Router {
|
||||
var path = NavigationPath()
|
||||
|
||||
func navigate(to destination: Destination) {
|
||||
path.append(destination)
|
||||
}
|
||||
|
||||
func popToRoot() {
|
||||
path = NavigationPath()
|
||||
}
|
||||
}
|
||||
|
||||
enum Destination: Hashable {
|
||||
case detail(Item.ID)
|
||||
case settings
|
||||
case profile(User.ID)
|
||||
}
|
||||
|
||||
struct RootView: View {
|
||||
@State private var router = Router()
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $router.path) {
|
||||
HomeView()
|
||||
.navigationDestination(for: Destination.self) { dest in
|
||||
switch dest {
|
||||
case .detail(let id): ItemDetailView(itemID: id)
|
||||
case .settings: SettingsView()
|
||||
case .profile(let id): ProfileView(userID: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
.environment(router)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
### Use Lazy Containers for Large Collections
|
||||
|
||||
`LazyVStack` and `LazyHStack` create views only when visible:
|
||||
|
||||
```swift
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 8) {
|
||||
ForEach(items) { item in
|
||||
ItemRow(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Stable Identifiers
|
||||
|
||||
Always use stable, unique IDs in `ForEach` — avoid using array indices:
|
||||
|
||||
```swift
|
||||
// Use Identifiable conformance or explicit id
|
||||
ForEach(items, id: \.stableID) { item in
|
||||
ItemRow(item: item)
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid Expensive Work in body
|
||||
|
||||
- Never perform I/O, network calls, or heavy computation inside `body`
|
||||
- Use `.task {}` for async work — it cancels automatically when the view disappears
|
||||
- Use `.sensoryFeedback()` and `.geometryGroup()` sparingly in scroll views
|
||||
- Minimize `.shadow()`, `.blur()`, and `.mask()` in lists — they trigger offscreen rendering
|
||||
|
||||
### Equatable Conformance
|
||||
|
||||
For views with expensive bodies, conform to `Equatable` to skip unnecessary re-renders:
|
||||
|
||||
```swift
|
||||
struct ExpensiveChartView: View, Equatable {
|
||||
let dataPoints: [DataPoint]
|
||||
|
||||
static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.dataPoints.count == rhs.dataPoints.count
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// Complex chart rendering
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Previews
|
||||
|
||||
Use `#Preview` macro with inline mock data for fast iteration:
|
||||
|
||||
```swift
|
||||
#Preview("Empty state") {
|
||||
ItemListView()
|
||||
.environment(ItemListViewModel(repository: EmptyMockRepository()))
|
||||
}
|
||||
|
||||
#Preview("Loaded") {
|
||||
ItemListView()
|
||||
.environment(ItemListViewModel(repository: PopulatedMockRepository()))
|
||||
}
|
||||
```
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- Using `ObservableObject` / `@Published` / `@StateObject` / `@EnvironmentObject` in new code — migrate to `@Observable`
|
||||
- Putting async work directly in `body` or `init` — use `.task {}` or explicit load methods
|
||||
- Creating view models as `@State` inside child views that don't own the data — pass from parent instead
|
||||
- Using `AnyView` type erasure — prefer `@ViewBuilder` or `Group` for conditional views
|
||||
- Ignoring `Sendable` requirements when passing data to/from actors
|
||||
|
||||
## References
|
||||
|
||||
See skill: `swift-actor-persistence` for actor-based persistence patterns.
|
||||
See skill: `swift-protocol-di-testing` for protocol-based DI and testing with Swift Testing.
|
||||
Reference in New Issue
Block a user