- name
- swiftui-patterns-developer
- description
- SwiftUI view structure, composition, and best practices. Use when refactoring SwiftUI views, organizing view files, or extracting subviews.
# SwiftUI Patterns Developer (Smart Router)
## Purpose
Apply consistent structure and patterns to SwiftUI views, with focus on ordering, subview extraction, and proper composition.
## When Auto-Activated
- Refactoring SwiftUI view structure
- Organizing view file layout
- Splitting large views into subviews
- Keywords: view structure, view ordering, split view, extract subview, large view, refactor view
## Core Guidelines
### 0) Three Qualities of SwiftUI Views (WWDC24)
Understanding these fundamentals helps you write better SwiftUI code:
**1. Declarative** - Describe what you want, not how to create it:
```swift
// ✅ Declarative - describe the result
List(pets) { pet in
HStack {
Text(pet.name)
Spacer()
Text(pet.species)
}
}
// No need to add/remove rows manually - SwiftUI handles it
```
**2. Compositional** - Build complex UIs from simple building blocks:
```swift
// ViewBuilder closures define children of containers
HStack { // Container view
Image(...) // Child 1
VStack { // Child 2 (also a container)
Text(...) // Nested child
Text(...) // Nested child
}
Spacer() // Child 3
}
```
**3. State-Driven** - UI automatically updates when state changes:
```swift
// SwiftUI tracks dependencies and updates views automatically
@State private var count = 0
var body: some View {
Button("Count: \(count)") { // Dependency on `count`
count += 1 // State change triggers re-render
}
}
```
**Key insight**: Views are VALUE TYPES (structs), not long-lived objects. They are descriptions of current UI state, not objects that receive commands over time. SwiftUI maintains the actual UI behind the scenes.
### 1) View Ordering (top -> bottom)
Follow Anytype's property organization from IOS_DEVELOPMENT_GUIDE.md:
```swift
struct ExampleView: View {
// 1. Property wrappers (@State, @Injected, @Environment)
@State private var model: ExampleViewModel
@Injected(\.settingsService) private var settingsService
@Environment(\.dismiss) private var dismiss
// 2. Public properties (let/var)
let title: String
// 3. Private properties
private var cancellables = Set<AnyCancellable>()
// 4. Computed properties
private var hasItems: Bool { !model.items.isEmpty }
// 5. init (if needed)
init(title: String) {
self.title = title
_model = State(wrappedValue: ExampleViewModel(title: title))
}
// 6. body
var body: some View {
content
.task { await model.startSubscriptions() }
}
// 7. Computed view builders
private var content: some View { ... }
// 8. Helper / async functions
private func handleTap() { ... }
}
```
### 2) ViewModel Pattern (Anytype Standard)
Anytype uses **MVVM with ViewModels**. Always use ViewModels for business logic:
```swift
// View - lightweight, UI only
struct ChatView: View {
@State private var model: ChatViewModel
init(spaceId: String, chatId: String) {
_model = State(wrappedValue: ChatViewModel(spaceId: spaceId, chatId: chatId))
}
var body: some View {
content
.task { await model.startSubscriptions() }
}
private var content: some View {
List(model.messages) { message in
MessageRow(message: message)
}
}
}
// ViewModel - handles business logic
@MainActor
@Observable
final class ChatViewModel {
var messages: [Message] = []
@ObservationIgnored
@Injected(\.chatService) private var chatService
func startSubscriptions() async {
// Heavy work here, not in init
}
func sendMessage(_ text: String) async {
// Business logic
}
}
```
**Key points:**
- Use `@State private var model: ViewModel` in views
- Initialize ViewModel in view's `init` with `_model = State(wrappedValue:)`
- Keep ViewModel init cheap, heavy work in `.task`
- Use `@Observable` macro (not `ObservableObject`)
- Mark ViewModels with `@MainActor`
### 3) How Observation Works (WWDC23)
Understanding **why** `@Observable` works helps you use it correctly.
**Property Access Tracking:**
- SwiftUI tracks which properties you **access** during `body` evaluation
- Only those accessed properties trigger view invalidation when changed
- Properties NOT read in `body` don't cause re-renders (unlike `@Published`)
```swift
@Observable
final class SettingsViewModel {
var userName: String = "" // Accessed in body → triggers update
var isLoading: Bool = false // Accessed in body → triggers update
var analyticsData: Data = Data() // NOT accessed in body → no update
}
struct SettingsView: View {
@State private var model: SettingsViewModel
var body: some View {
// SwiftUI tracks: "this view reads userName and isLoading"
VStack {
Text(model.userName) // ✓ Tracked
if model.isLoading { // ✓ Tracked
ProgressView()
}
// model.analyticsData not read → changes won't invalidate this view
}
}
}
```
**Per-Instance Tracking:**
- Arrays of `@Observable` objects work efficiently
- Only the specific instance that changed triggers updates
- No need for `identifiable` tricks with observation
```swift
@Observable
final class MessageViewModel {
var text: String
var isRead: Bool = false
}
// Each MessageRow only updates when ITS message changes
List(model.messages) { message in
MessageRow(message: message) // Only this row updates when message.isRead changes
}
```
**Computed Properties Just Work:**
- Computed properties composed from stored properties are automatically tracked
- SwiftUI traces through to the underlying stored properties
```swift
@Observable
final class CartViewModel {
var items: [Item] = []
var discount: Double = 0
// Computed → tracks both `items` and `discount`
var totalPrice: Double {
items.reduce(0) { $0 + $1.price } - discount
}
}
```
**Performance Benefit:**
With `@Observable`, views only update when properties they actually read change. This is more efficient than `ObservableObject` where ANY `@Published` change triggers `objectWillChange` for ALL subscribers.
### 4) Property Wrapper Decision Tree
When to use which wrapper with `@Observable`:
| Scenario | Wrapper | Why |
|----------|---------|-----|
| View **owns** model lifecycle | `@State` | View creates and manages the model |
| Model shared **app-wide** | `@Environment` | Injected at app root, read anywhere |
| Just need **bindings** ($syntax) | `@Bindable` | Pass to TextField, Toggle, etc. |
| Just **reading** the model | Nothing | Direct property access triggers tracking |
```swift
// View OWNS the model (creates it)
struct ChatView: View {
@State private var model: ChatViewModel // ← @State
init(chatId: String) {
_model = State(wrappedValue: ChatViewModel(chatId: chatId))
}
}
// Model passed from parent, need bindings
struct MessageEditor: View {
@Bindable var draft: DraftMessage // ← @Bindable for $draft.text
var body: some View {
TextField("Message", text: $draft.text)
}
}
// Just reading, no bindings needed
struct MessageRow: View {
let message: MessageViewModel // ← Nothing! Just read properties
var body: some View {
Text(message.text)
Image(systemName: message.isRead ? "checkmark.circle.fill" : "circle")
}
}
```
**Migration from ObservableObject:**
| Old | New |
|-----|-----|
| `@StateObject` | `@State` |
| `@ObservedObject` | `@Bindable` or nothing |
| `@EnvironmentObject` | `@Environment` |
### 5) Migration from ObservableObject (WWDC23)
Step-by-step conversion from legacy `ObservableObject`:
**Before (ObservableObject):**
```swift
class SettingsViewModel: ObservableObject {
@Published var userName: String = ""
@Published var notifications: Bool = true
private var cancellables = Set<AnyCancellable>()
}
struct SettingsView: View {
@StateObject private var model = SettingsViewModel()
var body: some View {
TextField("Name", text: $model.userName)
Toggle("Notifications", isOn: $model.notifications)
}
}
```
**After (@Observable):**
```swift
@Observable
final class SettingsViewModel {
var userName: String = ""
var notifications: Bool = true
@ObservationIgnored
private var cancellables = Set<AnyCancellable>()
}
struct SettingsView: View {
@State private var model = SettingsViewModel()
var body: some View {
@Bindable var model = model // Local binding for $ syntax
TextField("Name", text: $model.userName)
Toggle("Notifications", isOn: $model.notifications)
}
}
```
**Migration Steps:**
1. Remove `ObservableObject` conformance, add `@Observable` macro
2. Remove `@Published` from all properties (observation is automatic)
3. Add `@ObservationIgnored` to properties that shouldn't trigger updates
4. Change `@StateObject` → `@State` in views
5. For `$` binding syntax, use `@Bindable var model = model` in body
6. Replace `@EnvironmentObject` with `@Environment`
**Note:** Anytype already uses `@Observable` - this section is for understanding legacy code during migrations.
### 6) Dependency Injection (Factory)
Anytype uses **Factory DI**, not SwiftUI Environment for services:
```swift
// ✅ CORRECT - Factory DI
@Injected(\.chatService) private var chatService
// ❌ WRONG - Environment for services
@Environment(ChatService.self) private var chatService
```
**Environment is for:**
- System values: `@Environment(\.dismiss)`, `@Environment(\.colorScheme)`
- SwiftUI-provided context
**@Injected is for:**
- App services: `@Injected(\.chatService)`
- Repositories: `@Injected(\.userRepository)`
- Any business logic dependencies
### 7) View Modifiers and Order (WWDC24)
View modifiers create a hierarchical structure. **Order matters** - modifiers are applied sequentially:
```swift
// Each modifier wraps the previous result
Image("whiskers")
.clipShape(Circle()) // 1. Clip to circle first
.shadow(radius: 4) // 2. Add shadow to clipped shape
.overlay( // 3. Overlay on top of shadow
Circle().stroke(.green, lineWidth: 2)
)
```
The hierarchy and order of effect is defined by the exact order of modifiers. Chaining modifiers makes it clear how results are produced and how to customize them.
### 8) Adaptive Views (WWDC24)
SwiftUI views describe **purpose**, not exact visual construction. This enables adaptation:
**Buttons** - Same purpose (labeled action), different contexts:
```swift
// Adapts to: borderless, bordered, prominent styles
// Adapts to: swipe actions, menus, forms
Button("Edit", action: handleEdit)
// In swipe actions
.swipeActions {
Button("Delete", role: .destructive) { delete() }
Button("Archive") { archive() }
}
```
**Toggles** - Switch, checkbox, or toggle button depending on context:
```swift
// Automatically shows appropriate style for platform/context
Toggle("Notifications", isOn: $notificationsEnabled)
```
**Searchable** - Describes capability, SwiftUI handles idiomatic presentation:
```swift
// iOS: overlay list, macOS: dropdown menu
List(filteredItems) { ... }
.searchable(text: $searchText)
.searchSuggestions {
ForEach(suggestions) { Text($0) }
}
```
### 9) Split Large Bodies
If `body` grows beyond a screen, split into smaller subviews:
```swift
// Computed view properties (same file)
var body: some View {
List {
header
filters
results
}
}
private var header: some View { ... }
private var filters: some View { ... }
private var results: some View { ... }
```
```swift
// Extracted subview (reusable or complex)
struct HeaderSection: View {
let title: String
let subtitle: String?
var body: some View {
VStack(alignment: .leading, spacing: 4) {
AnytypeText(title, style: .heading)
if let subtitle {
AnytypeText(subtitle, style: .bodyRegular)
}
}
}
}
```
### 8) ViewState Enum Pattern
For views with loading/error/loaded states:
```swift
enum ViewState {
case loading
case error(String)
case loaded
}
@MainActor
@Observable
final class FeedViewModel {
var viewState: ViewState = .loading
var posts: [Post] = []
func loadPosts() async {
do {
posts = try await feedService.getFeed()
viewState = .loaded
} catch {
viewState = .error(error.localizedDescription)
}
}
}
struct FeedView: View {
@State private var model: FeedViewModel
var body: some View {
content
.task { await model.loadPosts() }
}
@ViewBuilder
private var content: some View {
switch model.viewState {
case .loading:
ProgressView()
case .error(let message):
ErrorView(message: message, retry: { Task { await model.loadPosts() } })
case .loaded:
List(model.posts) { post in
PostRow(post: post)
}
}
}
}
```
### 11) State, Binding, and Source of Truth (WWDC24)
**@State** creates internal source of data for a view:
```swift
struct RatingView: View {
@State private var rating = 0 // View owns this state
var body: some View {
HStack {
Text("\(rating)")
عرض على GitHub