# Kapilmhr Ios26 Swiftui Skills IOS

> iOS SwiftUI MVVM Skill

- Skill: `tomevault-io/kapilmhr-ios26-swiftui-skills-ios` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/kapilmhr-ios26-swiftui-skills-ios`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/kapilmhr-ios26-swiftui-skills-ios/raw
- Safety review: pending (external: skill-scanner PASS, skillspector WARNING)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/kapilmhr-ios26-swiftui-skills-ios

---


# iOS SwiftUI MVVM Skill

## Description

This skill defines the architecture, conventions, patterns, and templates for iOS application development using **SwiftUI + MVVM + async/await**. It enforces scope-based feature isolation, mandatory preview stubs, extension-heavy code style, and protocol-based dependency injection.

**Activates when:** The task involves SwiftUI views, iOS app features, MVVM architecture, Swift models, ViewModels, repositories, networking, navigation, or any iOS-specific development.

**Target:** iOS 17+ · SwiftUI · Swift 5.9+ · @Observable macro · Swift Concurrency · Swift Testing

---

## References

| File | Content |
|------|---------|
| `references/architecture-examples.md` | Full working code examples for every layer (Model, ViewModel, View, Repository, DI, App Root, Extensions, Error Handling, Persistence, Theming, AppRadius, Liquid Glass, Assets/Fonts, Testing) |
| `references/mvvm-templates.md` | Copy-paste scaffolding templates with `{Feature}` placeholders for quick feature creation |
| `references/routing-and-networking.md` | Navigation (Route enum, AppRouter, MainTabView, DeepLinkHandler) + NetworkService (Protocol, Impl, Endpoint, HTTPMethod) |
| `references/checklist.md` | Pre-submission verification checklist (Architecture, Models, ViewModels, Views, Naming, Errors, Testing, Design, Accessibility, Localisation, Components) |
| `../../docs/overview.md` | Full architecture philosophy, scope-based MVVM deep-dive, data flow diagrams, and working examples |

---

## Project Structure — Mandatory Layout

```
ProjectName/
├── App/
│   ├── ProjectNameApp.swift              # @main, .environment() setup
│   ├── AppState.swift                    # @Observable shared app state
│   ├── Launch/
│   │   └── SplashScreen.swift            # Splash / launch animation screen
│   └── DI/
│       └── DependencyContainer.swift     # Core service registration
│
├── Network/
│   ├── NetworkService.swift              # Protocol + URLSession implementation
│   ├── Endpoint.swift                    # Enum-based API endpoint definitions
│   ├── HTTPMethod.swift                  # HTTP method enum
│   └── NetworkError.swift                # Network-layer error types
│
├── Core/
│   ├── Storage/
│   │   ├── SessionService.swift
│   │   └── KeychainService.swift
│   ├── Extensions/
│   │   ├── ViewModifiers.swift
│   │   ├── DateFormatting.swift
│   │   ├── StringValidation.swift
│   │   ├── ColorTheme.swift
│   │   └── URLRequestBuilder.swift
│   ├── Components/
│   │   ├── AppButton.swift
│   │   ├── AppTextField.swift
│   │   ├── LoadingView.swift
│   │   └── ErrorView.swift
│   ├── Theme/
│   │   ├── AppTheme.swift
│   │   ├── AppSpacing.swift
│   │   └── AppRadius.swift
│   ├── Utils/
│   │   ├── AppError.swift
│   │   └── ResultExtensions.swift
│   └── Constants/
│       ├── AppConstants.swift
│       ├── ApiEndpoints.swift
│       └── AppStrings.swift              # Shared localized string keys
│
├── Features/
│   └── {Feature}/
│       ├── Model/
│       │   ├── {Feature}Model.swift
│       │   └── {Feature}ModelStub.swift
│       ├── View/
│       │   ├── {Feature}Screen.swift
│       │   ├── {Feature}View.swift
│       │   └── {Feature}Row.swift        # Feature-specific components (flat, no subfolder)
│       ├── ViewModel/
│       │   ├── {Feature}ViewModel.swift
│       │   └── {Feature}ViewModelStub.swift
│       └── Repository/
│           ├── {Feature}Repository.swift
│           └── {Feature}RepositoryImpl.swift
│
├── Shared/
│   ├── Models/
│   ├── Views/
│   └── Modifiers/
│
├── Navigation/
│   ├── AppRouter.swift
│   ├── Route.swift
│   ├── MainTabView.swift
│   └── DeepLinkHandler.swift
│
└── Resources/
    ├── Assets.xcassets
    └── Localizable.xcstrings
```

---

## Naming Conventions

| Item | Convention | Example |
|------|-----------|---------|
| Feature folder | PascalCase singular | `Product/`, `Auth/`, `Profile/` |
| Model file | `{Feature}Model.swift` | `ProductModel.swift` |
| Model stub | `{Feature}ModelStub.swift` | `ProductModelStub.swift` |
| ViewModel file | `{Feature}ViewModel.swift` | `ProductViewModel.swift` |
| ViewModel stub | `{Feature}ViewModelStub.swift` | `ProductViewModelStub.swift` |
| Screen (owns VM) | `{Feature}Screen.swift` | `ProductScreen.swift` |
| View (pure UI) | `{Feature}{Purpose}View.swift` | `ProductListView.swift` |
| Repository protocol | `{Feature}Repository.swift` | `ProductRepository.swift` |
| Repository impl | `{Feature}RepositoryImpl.swift` | `ProductRepositoryImpl.swift` |
| Extension file | `TypePurpose.swift` | `ViewModifiers.swift`, `DateFormatting.swift` |
| Component (shared) | `App{Component}.swift` | `AppButton.swift`, `AppTextField.swift` |
| Component (feature) | `{Feature}{Component}.swift` | `ProductRow.swift`, `ProductPriceTag.swift` |

**CRITICAL:** No `+` character in ANY filename. Use `TypePurpose.swift` not `Type+Purpose.swift`.

---

## MVVM Architecture

```
View Layer          →  ViewModel Layer       →  Data Layer
(SwiftUI Views)       (@Observable classes)     (Repository + NetworkService)

- Renders UI          - Holds state            - Fetches data
- Dispatches actions  - Async business logic   - Maps responses
- Observes state      - Calls repository       - Returns Result<T, AppError>
```

### Architecture Rules

1. **Views never call NetworkService directly** — always through ViewModel → Repository
2. **ViewModels never import SwiftUI** — only `Foundation` and domain types
3. **Repositories never hold UI state** — they return data, ViewModels manage state
4. **NetworkService is generic** — it doesn't know about specific models
5. **Each layer depends only on the layer below it** — no circular dependencies

---

## State Management Rules

| Wrapper | Where | When |
|---------|-------|------|
| `@State` | View | Local value-type state OR owning an `@Observable` ViewModel |
| `@Binding` | Child View | Two-way communication with parent |
| `@Bindable` | View | Creating bindings to `@Observable` ViewModel properties |
| `@Environment` | View | Accessing shared dependencies (DependencyContainer, AppState, AppRouter) |

- All ViewModels use `@Observable @MainActor` — no `@Published`, no `ObservableObject`
- No `@StateObject`, `@ObservedObject` for new code (iOS 17+)
- `ObservableObject` only as fallback when supporting pre-iOS 17

> See `references/architecture-examples.md` for full state management examples and anti-patterns.

---

## Model Rules

1. Conform to `Codable` + `Identifiable` + `Hashable` (implies `Equatable`)
2. Use `CodingKeys` when API field names differ from Swift property names
3. All properties are `let` unless mutation is required
4. Use optionals for truly optional API fields — don't default to empty strings
5. One model per file — named `{Feature}Model.swift`
6. **No logic in models** — pure data containers
7. **Every model must have a companion `{Feature}ModelStub.swift`**
8. **Components MUST accept model types** — never pass decomposed primitives (`String`, `Int`, `URL?`) as separate parameters when a model exists
9. **ModelStub is the ONLY data source for previews** — no inline literals, no hardcoded values, no "example" strings anywhere in `#Preview` blocks

### Stub Requirements

- Wrapped in `#if DEBUG`
- Provide `static var stub: Self` — single instance with realistic data
- Provide `static var stubs: [Self]` — array with 3+ items showing variety
- Provide named edge-case variants for preview coverage:
  - `static var stubLongText: Self` — maximum-length strings to test truncation
  - `static var stubNilOptionals: Self` — all optional fields set to `nil`
  - `static var stubEmpty: Self` — empty collections, blank strings
- Include boundary values: dates at epoch, zero counts, maximum IDs

> See `references/mvvm-templates.md` for Model and ModelStub templates.

---

## ViewModel Rules

1. **Always** use `@Observable` macro
2. **Always** annotate with `@MainActor` — ensures all state mutations are main-thread-safe
3. **Never** import `SwiftUI`
4. Receive repository via init (protocol-typed)
5. All data methods are `async` — no callbacks, no Combine
6. Use `defer { isLoading = false }` for loading state cleanup
7. Handle both `.success` and `.failure` from repository results
8. Expose `var errorMessage: String?` for error display
9. Methods are imperative verbs: `fetchProducts()`, `deleteProduct()`, `submitForm()`
10. **Every ViewModel must have a companion `{Feature}ViewModelStub.swift`**

### ViewModel Stub Requirements

- Wrapped in `#if DEBUG`
- Provide `static func stub(...)` with default parameters for all observable state
- Include a private `StubRepository` class that returns model stubs
- `stub()` works with zero arguments using `ModelStub` defaults

> See `references/mvvm-templates.md` for ViewModel and ViewModelStub templates.

---

## View Rules

### Screen vs View Pattern

| Widget | Responsibility | Owns ViewModel? |
|--------|---------------|-----------------|
| **Screen** | Creates/owns ViewModel, provides to children, triggers initial load | Yes — via `@State` |
| **View** | Pure UI rendering, receives ViewModel or data, no lifecycle logic | No — receives as parameter |

### View Requirements

1. **Every view and component file must have `#Preview` blocks** covering all relevant states:
   - `#Preview` — default / populated state
   - `#Preview("Loading")` — loading indicator visible
   - `#Preview("Empty")` — empty state messaging
   - `#Preview("Error")` — error state with message
   - `#Preview("Edge Case")` — long text, nil optionals, boundary data (use `ModelStub` edge variants)
   - `#Preview("Dark Mode")` — with `.preferredColorScheme(.dark)`
2. Use `#Preview("StateName")` labels to clearly identify each variant
3. **CRITICAL — No inline data in previews.** Previews MUST use `ViewModelStub.stub()` or `ModelStub.stub` exclusively. NEVER pass hardcoded strings, numbers, or literal values as preview parameters.

```swift
// ❌ NEVER — inline data in preview
#Preview {
    HomeScreen(title: "Welcome", userName: "John", itemCount: 5)
}

// ✅ ALWAYS — stub-driven preview
#Preview {
    HomeScreen(viewModel: .stub())
}
```

4. **Components accept Model types, not primitives** — a row takes `ProductModel`, not `(title: String, price: Double)`. If a component needs data, pass the model struct.
5. Use `.task { }` for async work on appear — never `onAppear` with Task
6. Use `.refreshable { }` for pull-to-refresh
7. Keep `body` concise — extract subviews as computed properties or separate structs (see Component Decomposition Rules)
8. No business logic in `body` — all logic lives in ViewModel
9. Use `AppSpacing` constants — no magic numbers
10. Use `AppRadius` constants — no raw corner radius values
11. Use semantic system colors (`.primary`, `.secondary`) or `AppTheme`

> See `references/architecture-examples.md` for Screen, View, and Component examples.
> See `references/mvvm-templates.md` for scaffolding templates.

---

## Component Decomposition Rules

### CRITICAL — Decompose During Initial Generation

**DO NOT write a monolithic Screen.** When creating a new feature, plan and create separate component files UPFRONT — not as a later refactoring step.

- A Screen's `body` should be **≤50 lines** — it orchestrates components, not renders UI directly
- Every distinct UI section (header, card, row, list, banner, stat block) MUST be its own struct in a separate file under `View/`
- Each extracted component MUST have its own `#Preview` with `ModelStub.stub` data

```swift
// ❌ NEVER — everything in one Screen file
struct HomeScreen: View {
    var body: some View {
        ScrollView {
            // 200+ lines of header, cards, lists, banners...
        }
    }
}

// ✅ ALWAYS — Screen orchestrates, components render
struct HomeScreen: View {
    @State private var viewModel: HomeViewModel

    var body: some View {
        ScrollView {
            HomeHeaderView(user: viewModel.user)
            HomeFeaturedCard(item: viewModel.featuredItem)
            HomeRecentList(items: viewModel.recentItems)
        }
        .task { await viewModel.fetch() }
    }
}
// Each component lives in its own file with its own #Preview
```

### When to Extract a Component

Extract a piece of UI into its own file when **any** of these triggers apply:

| Trigger | Threshold |
|---------|-----------|
| Line count | View `body` or subview exceeds **~50 lines** (extract immediately) |
| Identity | The piece has its own data model or identity (`Identifiable`) |
| State | It manages its own `@State` or `@Binding` |
| Reuse | Used ≥2 times within the feature or across features |
| Preview need | Needs isolated preview states (loading, error, edge case) |
| Distinct section | Visually distinct UI block (header, card, row, banner, stat) |

### Placement Rules

| Scope | Location | Example |
|-------|----------|---------|
| Shared across features | `Core/Components/` | `AppButton.swift`, `LoadingView.swift`, `ErrorView.swift` |
| Feature-specific | `Features/{Feature}/View/` (flat — no nested subfolder) | `ProductRow.swift`, `ProductPriceTag.swift` |

### Requirements

1. **One component per file** — named `{Feature}{Component}.swift` (feature) or `App{Component}.swift` (shared)
2. **Every component file must have its own `#Preview`** with stub data — no exceptions
3. **Components accept model structs** — never raw primitives as parameters. Pass `ProductModel` not `(title: String, price: Double, imageURL: URL?)`
4. **NEVER use inline literals in component previews** — always `ModelStub.stub` or `ModelStub.stubs`. If a stub doesn't exist, create `{Feature}ModelStub.swift` FIRST before writing the preview
5. **Before writing ANY `#Preview` block**, verify the corresponding `{Feature}ModelStub.swift` exists. If it doesn't — stop and create it
6. **No nested `Components/` subfolder** inside feature `View/` directories — keep flat
7. **Inline private structs** are allowed only if <20 lines with zero `@State` — otherwise extract to own file

```swift
// ❌ NEVER — component with loose parameters
struct HomeItemRow: View {
    let title: String
    let subtitle: String
    let imageURL: URL?
}

// ✅ ALWAYS — component accepts model
struct HomeItemRow: View {
    let item: HomeItemModel
}

// ❌ NEVER — inline preview data
#Preview {
    HomeItemRow(title: "Hello", subtitle: "World", imageURL: nil)
}

// ✅ ALWAYS — stub data
#Preview {
    HomeItemRow(item: .stub)
}
```

### Migration Note

Existing features with nested `Components/` subfolders should be flattened to `View/` during refactoring.

> See `references/architecture-examples.md` for Component examples.

---

## Repository Rules

1. **Always** define a protocol (abstract interface)
2. **Always** create a separate `Impl` class (concrete)
3. Return `Result<T, AppError>` — never throw from public methods
4. Repository is stateless — no held state
5. Takes `NetworkService` via init (protocol-typed)
6. ViewModel depends on the protocol — enables mock injection

> See `references/architecture-examples.md` for Repository protocol + implementation examples.

---

## NetworkService Rules

1. Protocol-defined — enables mock injection
2. Returns `Result<T, AppError>` — never throws from public interface
3. Token injection via `SessionService` automatically
4. 401 responses auto-clear session
5. `Endpoint` enum is single source of truth for API paths
6. JSON decoding uses `.iso8601` date strategy

> See `references/routing-and-networking.md` for full NetworkService, Endpoint, and HTTPMethod implementations.

---

## Extension Rules

1. **Naming:** `TypePurpose.swift` in `Core/Extensions/` — no `+` in filename
2. **Stubs:** `{Feature}ModelStub.swift` in feature `Model/` folder
3. Extensions must be pure — no stored state, no side effects
4. Group related extensions in one file
5. Prefer extensions over global functions

> See `references/architecture-examples.md` for ViewModifiers, DateFormatting, StringValidation examples.

---

## Error Handling Rules

1. All repositories return `Result<T, AppError>` — never throw
2. ViewModels switch on Result and surface `errorMessage`
3. Views display errors via `.alert` or `ErrorView` component
4. No silent failures — every error shows to user or is logged
5. Network errors are human-readable — never raw error strings
6. Use `defer { isLoading = false }` to guarantee cleanup

> See `references/architecture-examples.md` for AppError + NetworkErrorReason implementations.

---

## Navigation Rules

1. All routes defined in `Route` enum — no string-based navigation
2. Use `NavigationStack` with `NavigationPath` — not `NavigationView`
3. `AppRouter` injected via `.environment()`
4. Associated values for route parameters
5. Sheets and full-screen covers managed through `AppRouter`
6. Deep links convert URLs to `Route` enum values

> See `references/routing-and-networking.md` for Route, AppRouter, MainTabView, and DeepLinkHandler.

---

## App Root Structure

Flow: **Splash → Auth Check → Main App (TabView) or Login**

1. `SplashScreen` is the first view — handles auth gate
2. Each tab has its own `NavigationStack`
3. Splash performs startup: token validation, cache warm-up, version check
4. `AppState.isAuthenticated` drives root-level conditional rendering
5. Use `withAnimation` for smooth splash → main transition

> See `references/architecture-examples.md` for SplashScreen template.

---

## Environment Configuration Rules

1. Use `#if DEBUG` for compile-time environment switching
2. For staging: Xcode scheme + custom build flags (`-D STAGING`)
3. `AppConfig.baseURL` is the single source for API host
4. No secrets in source code — use `.xcconfig` files
5. Feature flags as static properties in `AppConfig`

> See `references/architecture-examples.md` for AppConfig implementation.

---

## DI & Environment Rules

1. `DependencyContainer` is `@Observable` — injected at App root via `.environment()`
2. All services are protocol-typed
3. Repositories are `lazy var` — created on first access
4. Views access container via `@Environment(DependencyContainer.self)`
5. For previews, mock repos live in `ViewModelStub` — no need to mock container
6. `AppState` holds auth status and shared user data
7. `AppRouter` holds navigation state

> See `references/architecture-examples.md` for DependencyContainer, AppState, and App entry point.

---

## Persistence Rules

1. All storage access wrapped behind protocols
2. Keys in private enum — no scattered string literals
3. **NEVER store tokens in UserDefaults** — it is plaintext on disk. Use Keychain for all sensitive data (access tokens, refresh tokens, credentials)
4. Non-sensitive preferences (flags, theme, last-viewed IDs) use `UserDefaults` wrapped in `SessionService`
5. `clearSession()` removes all auth data atomically
6. For structured local data (offline cache, drafts), use **SwiftData** with `@Model` — see SwiftData rules below

> See `references/architecture-examples.md` for SessionService protocol + implementation.

---

## Theming Rules

1. **Never** hard-coded spacing — always `AppSpacing.md`, `AppSpacing.lg`, etc.
2. **Never** hard-coded colors — system colors (`.primary`, `.secondary`) or `AppTheme`
3. **Never** hard-coded corner radii — always `AppRadius.sm`, `AppRadius.md`, `AppRadius.lg`, etc.
4. **Color assets** must define light, dark, and tinted variants in the asset catalog
5. **Contrast ratios** documented as comments next to every color definition in `AppTheme`
6. **Typography** must use scalable system fonts (`.body`, `.headline`) or `.custom(..., relativeTo:)` — no fixed `size:` values
7. Prefer system semantic colors for accessibility and dark mode
8. All design tokens are value types — `CGFloat` constants in `AppSpacing`, `AppRadius`, `AppTheme`

### AppRadius Constants

```swift
// Core/Theme/AppRadius.swift
import SwiftUI

enum AppRadius {
    static let xs: CGFloat = 4
    static let sm: CGFloat = 8
    static let md: CGFloat = 12
    static let lg: CGFloat = 16
    static let xl: CGFloat = 24
    static let full: CGFloat = .infinity  // Capsule
}

// Usage
.clipShape(RoundedRectangle(cornerRadius: AppRadius.md))
.background(Color.appSurface, in: RoundedRectangle(cornerRadius: AppRadius.lg))
```

> See `references/architecture-examples.md` for AppSpacing, AppRadius, and AppTheme.

---

## Accessibility Rules — WCAG AA

WCAG AA is the **enforced baseline**. AAA (7:1 contrast) is aspirational.

### Mandatory Requirements

| Rule | Implementation |
|------|---------------|
| Labels on interactive elements | `.accessibilityLabel("Description")` on every Button, Link, Toggle, custom control |
| Hints for non-obvious actions | `.accessibilityHint("Double-tap to...")` when the action isn't clear from label alone |
| Decorative images hidden | `.accessibilityHidden(true)` on all decorative/background images |
| Semantic traits | `.accessibilityAddTraits(.isHeader)` on section titles, `.isButton` on custom tap targets, `.isSelected` on active states |
| Touch targets | Minimum **44×44pt** — use `.frame(minWidth: 44, minHeight: 44)` or `.contentShape(Rectangle())` |
| Dynamic Type | **No fixed font sizes** — use system styles (`.body`, `.headline`) or `.custom(..., relativeTo:)` |
| Contrast — body text | **4.5:1** minimum against background |
| Contrast — large text & UI components | **3:1** minimum |
| No colour-only meaning | Always pair colour indicators with an icon, label, or pattern |

### Testing Requirements

- Test with **VoiceOver** enabled — verify reading order and labels
- Test at **AX5** (largest Dynamic Type) — no truncation of critical content
- Test with **Reduce Motion** — disable spring animations, use crossfade
- Test with **Increase Contrast** — verify all elements remain visible
- Test with **Bold Text** — verify layout doesn't break

### Anti-Patterns

```swift
// ❌ No label — VoiceOver reads "button"
Button(action: delete) {
    Image(systemName: "trash")
}

// ✅ Accessible
Button(action: delete) {
    Image(systemName: "trash")
}
.accessibilityLabel("Delete item")
.accessibilityHint("Removes this item permanently")

// ❌ Colour-only status
Circle().fill(isActive ? .green : .red)

// ✅ Colour + icon
HStack {
    Image(systemName: isActive ? "checkmark.circle.fill" : "xmark.circle.fill")
    Text(isActive ? "Active" : "Inactive")
}
.foregroundStyle(isActive ? .green : .red)

// ❌ Fixed font size
Text("Title").font(.system(size: 24))

// ✅ Scalable
Text("Title").font(.title)
```

---

## Localisation Rules

### String Handling

1. **All user-facing strings** must use `LocalizedStringKey` constants — never hardcode strings in views
2. **Shared strings** live in `Core/Constants/AppStrings.swift`
3. **Feature-specific strings** live in `Features/{Feature}/Strings{Feature}.swift`
4. **Every key** must exist in `Localizable.xcstrings` with English as the default language
5. **Adding a new string** = adding the key constant + English value to the catalog in the same commit
6. Use `String(localized:)` for non-view contexts (ViewModels, services)
7. **Never concatenate** localized strings — use interpolation: `"Hello, \(name)"` inside `LocalizedStringKey`
8. Plurals and grammar rules use String Catalog plural variants

### Pattern

```swift
// Core/Constants/AppStrings.swift
import SwiftUI

enum AppStrings {
    enum General {
        static let ok: LocalizedStringKey = "general_ok"
        static let cancel: LocalizedStringKey = "general_cancel"
        static let error: LocalizedStringKey = "general_error"
        static let retry: LocalizedStringKey = "general_retry"
    }
    
    enum Product {
        static let title: LocalizedStringKey = "product_title"
        static let emptyState: LocalizedStringKey = "product_empty_state"
        static func itemCount(_ count: Int) -> LocalizedStringKey {
            "product_item_count \(count)"
        }
    }
}

// Usage in View
Text(AppStrings.Product.title)
Button(AppStrings.General.retry) { await viewModel.fetch() }

// Usage in ViewModel (non-view context)
errorMessage = String(localized: "network_error_no_connection")
```

### Anti-Patterns

```swift
// ❌ Hardcoded string in view
Text("No products found")

// ❌ Concatenation
Text("Hello, " + username + "!")

// ❌ Key without catalog entry
Text("some_key_that_doesnt_exist_in_xcstrings")

// ✅ Correct
Text(AppStrings.Product.emptyState)
```

---

## Liquid Glass & iOS 26 Design Rules

Liquid Glass is the primary design material in iOS 26. It forms a distinct **functional layer** for controls and navigation elements that floats above content.

### Core Principles

1. **Use standard components** — NavigationStack, TabView, toolbars, sheets, popovers auto-adopt Liquid Glass
2. **Don't apply Liquid Glass in the content layer** — it's for navigation/controls only, not content views
3. **Use sparingly on custom views** — limit `.glassEffect()` to the most important functional elements
4. **Remove custom bar backgrounds** — let the system handle toolbar, tab bar, and navigation bar appearance
5. **Don't layer Liquid Glass on Liquid Glass** — avoid overcrowding or stacking glass elements
6. **Use `.scrollEdgeEffectStyle()` for content beneath bars** — maintains legibility automatically
7. **Prefer system button styles** — use `.buttonStyle(.glass)` or `.buttonStyle(.glassProminent)` instead of custom glass
8. **Test with accessibility settings** — reduced transparency, increased contrast, preferred Liquid Glass look

### Liquid Glass Variants

| Variant | When | API |
|---------|------|-----|
| **Regular** | Default — blurs/adjusts luminosity for legibility. Alerts, sidebars, popovers | `.glassEffect(.regular)` |
| **Clear** | Over visually rich backgrounds (photos, videos) — highly translucent | `.glassEffect(.clear)` |

### SwiftUI APIs

```swift
// Basic glass effect on custom control
Text("Label")
    .padding()
    .glassEffect(in: .capsule)

// Rounded rectangle shape
Text("Label")
    .padding()
    .glassEffect(in: .rect(cornerRadius: 16))

// Tinted + interactive (reacts to touch)
Image(systemName: "play.fill")
    .padding()
    .glassEffect(.regular.tint(.blue).interactive())

// Button styles
Button("Action") { }
    .buttonStyle(.glass)

Button("Primary") { }
    .buttonStyle(.glassProminent)

// Container for multiple glass elements (performance + morphing)
GlassEffectContainer(spacing: 20) {
    HStack(spacing: 20) {
        ForEach(items) { item in
            ItemView(item: item)
                .glassEffect()
                .glassEffectID(item.id, in: namespace)
        }
    }
}

// Tab bar minimization on scroll
TabView { /* ... */ }
    .tabBarMinimizeBehavior(.onScrollDown)

// Scroll edge effect for custom bars
CustomBar()
    .safeAreaBar(edge: .bottom) { content }
```

### Layout & Controls Changes (iOS 26)

1. Lists/forms have **larger row height and padding** — don't fight system metrics
2. Section corners are **more rounded** — concentric with hardware
3. Section headers use **title-style capitalization** — not all caps
4. Controls (sliders, toggles) adopt glass on interaction — no custom glass needed
5. Action sheets **originate from source element** — always specify source view/item
6. Sheets have **increased corner radius** and are inset from display edge
7. Use `ConcentricRectangle` / `.rect(corners:isUniform:)` for shapes matching hardware curvature

### What NOT to Do

```swift
// ❌ Applying glass to content layer
List { ... }
    .glassEffect() // WRONG — glass is for controls/nav, not content

// ❌ Custom toolbar backgrounds that override system
.toolbar { ... }
.toolbarBackground(.visible, for: .navigationBar)
.toolbarBackground(Color.blue, for: .navigationBar)  // Remove this

// ❌ Stacking glass on glass
VStack {
    Text("Item").glassEffect()
    Text("Another").glassEffect()  // Too many — use GlassEffectContainer or reduce
}

// ✅ Let system handle it — standard components get glass automatically
NavigationStack { ... }  // Tab bars, toolbars get glass free
```

> See `references/architecture-examples.md` for full Liquid Glass code patterns.

---

## Assets & Fonts Rules

1. **SF Symbols** are the primary icon system — use `Image(systemName:)` first
2. Custom assets go in `Assets.xcassets` with proper organization (image sets, color sets)
3. Support **light/dark/tinted** variants for all color assets
4. Custom fonts registered in Info.plist — access via type-safe `Font` extension
5. Prefer **system Dynamic Type** styles (`.headline`, `.body`) — custom fonts only when brand requires
6. All custom images must include **@2x and @3x** variants
7. Use **Symbol Effects** for animated SF Symbols (`.symbolEffect(.bounce)`, `.symbolEffect(.pulse)`)
8. App icons must provide **layered assets** for Liquid Glass icon effects (foreground, middle, background layers)

### Font Extension Pattern

```swift
// Core/Extensions/AppFonts.swift
import SwiftUI

extension Font {
    static func appFont(_ style: AppFontStyle, size: CGFloat) -> Font {
        .custom(style.rawValue, size: size, relativeTo: style.textStyle)
    }
}

enum AppFontStyle: String {
    case regular = "BrandFont-Regular"
    case medium = "BrandFont-Medium"
    case bold = "BrandFont-Bold"
    
    var textStyle: Font.TextStyle {
        switch self {
        case .regular: return .body
        case .medium: return .headline
        case .bold: return .title
        }
    }
}
```

> See `references/architecture-examples.md` for asset organization and SF Symbol patterns.

---

## Testing Rules

1. Use Swift Testing (`@Test`, `#expect`, `#require`, `@Suite`) — not XCTest
2. Mock via protocol conformance — no third-party frameworks
3. Reuse `Model.stub` / `.stubs` — same data as previews
4. Test success and failure paths for every async method
5. ViewModel tests are highest priority
6. Use configurable stub repositories
7. Use `#require` for unwrapping optionals — fails test immediately if nil
8. `@Suite` runs tests in parallel by default — keep tests independent

> See `references/architecture-examples.md` for full test suite example.
> See `references/mvvm-templates.md` for test suite template.

---

## Concurrency & Sendable Rules

Swift 6 strict concurrency is the target. Prepare all new code for full compliance.

1. **ViewModels** — `@MainActor @Observable` handles thread safety automatically
2. **Models** — `Codable` + `Hashable` structs with `let` properties are implicitly `Sendable`
3. **Repositories** — mark `final class` implementations as `Sendable` when they hold no mutable state (only `let` dependencies)
4. **NetworkService** — mark protocol methods as `sending` where applicable; implementation is `Sendable` (stateless URLSession wrapper)
5. **Never use `@unchecked Sendable`** unless wrapping a proven-safe third-party type — document why
6. **Closures crossing actor boundaries** must be `@Sendable` — captured values must be `Sendable`
7. Enable `-strict-concurrency=complete` in build settings for new projects

### Common Patterns

```swift
// ✅ Repository is Sendable — all dependencies are let + Sendable
final class ProductRepositoryImpl: ProductRepository, Sendable {
    private let networkService: NetworkService  // protocol is Sendable
    
    init(networkService: NetworkService) {
        self.networkService = networkService
    }
}

// ✅ Model structs with let properties are implicitly Sendable
struct ProductModel: Codable, Identifiable, Hashable, Sendable {
    let id: String
    let name: String
}

// ❌ WRONG — mutable state makes this non-Sendable
class BadRepository {
    var cachedItems: [ProductModel] = []  // Not safe across actors
}
```

---

## Import Organization Rules

Imports are organized in groups, separated by a blank line, alphabetically within each group:

```swift
// 1. Foundation / system frameworks
import Foundation
import SwiftUI      // Only in Views — never in ViewModels

// 2. Third-party packages (if any)
import Kingfisher

// 3. Project modules (for multi-module projects)
import NetworkKit
import SharedModels
```

### Rules

1. **ViewModels** import only `Foundation` (and domain module if multi-module)
2. **Views** import `SwiftUI` — which re-exports `Foundation`
3. **Repositories** import only `Foundation`
4. **Never import UIKit** unless wrapping a UIKit component in `UIViewRepresentable`
5. Remove unused imports — Xcode warns about these
6. No `@testable import` outside of test targets

---

## Task Cancellation Rules

1. **Prefer `.task { }` modifier** — SwiftUI automatically cancels when view disappears
2. **Manual `Task {}` blocks** in event handlers are NOT auto-cancelled — store and cancel explicitly if needed
3. **Check cancellation** in long loops: `try Task.checkCancellation()` or `guard !Task.isCancelled`
4. **Never ignore cancellation** — let `CancellationError` propagate or handle gracefully

### Patterns

```swift
// ✅ .task handles cancellation automatically — preferred
struct ProductScreen: View {
    var body: some View {
        ProductListView(viewModel: viewModel)
            .task { await viewModel.fetchProducts() }       // Cancelled on disappear
            .task(id: searchQuery) { await viewModel.search(searchQuery) }  // Re-runs + cancels previous
    }
}

// ✅ Manual cancellation for user-triggered Tasks
@Observable @MainActor
class SearchViewModel {
    private var searchTask: Task<Void, Never>?
    
    func search(_ query: String) {
        searchTask?.cancel()  // Cancel previous search
        searchTask = Task {
            try? await Task.sleep(for: .milliseconds(300))  // Debounce
            guard !Task.isCancelled else { return }
            await performSearch(query)
        }
    }
}

// ❌ WRONG — Task leaks, never cancelled
Button("Load") {
    Task { await viewModel.fetchProducts() }  // OK for one-shot actions
    // But DON'T do this for repeating/cancellable work
}
```

### ViewModel Cancellation

- One-shot actions (fetch, delete, submit) — no manual cancellation needed; `.task` or short-lived
- Ongoing/repeating work (search-as-you-type, polling) — store `Task` reference + cancel on new input
- Long operations (file upload, multi-page sync) — check `Task.isCancelled` between steps

---

## Pagination Pattern

For paginated / infinite scroll lists:

### State

```swift
@Observable @MainActor
class ProductViewModel {
    var products: [ProductModel] = []
    var isLoading = false
    var isLoadingMore = false
    var hasMore = true
    var errorMessage: String?
    
    private var currentPage = 1
    private let pageSize = 20
    private let repository: ProductRepository
    
    init(repository: ProductRepository) {
        self.repository = repository
    }
    
    func fetchProducts() async {
        isLoading = true
        defer { isLoading = false }
        currentPage = 1
        
        let result = await repository.getProducts(page: 1, size: pageSize)
        switch result {
        case .success(let data):
            products = data
            hasMore = data.count >= pageSize
            errorMessage = nil
        case .failure(let error):
            errorMessage = error.userMessage
        }
    }
    
    func loadMore() async {
        guard hasMore, !isLoadingMore else { return }
        isLoadingMore = true
        defer { isLoadingMore = false }
        
        let nextPage = currentPage + 1
        let result = await repository.getProducts(page: nextPage, size: pageSize)
        switch result {
        case .success(let data):
            products.append(contentsOf: data)
            currentPage = nextPage
            hasMore = data.count >= pageSize
        case .failure(let error):
            errorMessage = error.userMessage
        }
    }
}
```

### View Trigger

```swift
List(viewModel.products) { product in
    ProductRow(product: product)
        .onAppear {
            if product.id == viewModel.products.last?.id {
                Task { await viewModel.loadMore() }
            }
        }
}
```

---

## Form Handling Pattern

### Rules

1. **One ViewModel per form** — holds field values + validation state
2. **Validate on submit** by default. Real-time validation only for critical fields (email format, password strength)
3. `FocusState` lives in the View — not the ViewModel
4. ViewModel exposes `isFormValid: Bool` computed property for submit button state

### Form ViewModel

```swift
@Observable @MainActor
class CreateProductViewModel {
    // Field values
    var name = ""
    var description = ""
    var price = ""
    
    // Validation errors
    var nameError: String?
    var priceError: String?
    
    // Submit state
    var isSubmitting = false
    var errorMessage: String?
    var didSubmitSuccessfully = false
    
    var isFormValid: Bool {
        name.trimmed.isNotEmpty && Double(price) != nil
    }
    
    private let repository: ProductRepository
    
    init(repository: ProductRepository) {
        self.repository = repository
    }
    
    func submit() async {
        // Validate
        nameError = name.trimmed.isEmpty ? "Name is required" : nil
        priceError = Double(price) == nil ? "Enter a valid price" : nil
        guard nameError == nil, priceError == nil else { return }
        
        // Submit
        isSubmitting = true
        defer { isSubmitting = false }
        
        let product = ProductModel(
            id: UUID().uuidString,
            name: name.trimmed,
            description: description.trimmed,
            price: Double(price) ?? 0,
            imageURL: nil,
            category: "General",
            isAvailable: true,
            createdAt: .now
        )
        
        let result = await repository.createProduct(product)
        switch result {
        case .success:
            didSubmitSuccessfully = true
        case .failure(let error):
            errorMessage = error.userMessage
        }
    }
}
```

### Form View

```swift
struct CreateProductScreen: View {
    @State private var viewModel: CreateProductViewModel
    @FocusState private var focusedField: Field?
    @Environment(\.dismiss) private var dismiss
    
    enum Field { case name, description, price }
    
    init(viewModel: CreateProductViewModel) {
        _viewModel = State(initialValue: viewModel)
    }
    
    var body: some View {
        Form {
            Section("Details") {
                TextField("Product Name", text: $viewModel.name)
                    .focused($focusedField, equals: .name)
                if let error = viewModel.nameError {
                    Text(error).font(.caption).foregroundStyle(.red)
                }
                
                TextField("Description", text: $viewModel.description, axis: .vertical)
                    .focused($focusedField, equals: .description)
                    .lineLimit(3...6)
                
                TextField("Price", text: $viewModel.price)
                    .focused($focusedField, equals: .price)
                    .keyboardType(.decimalPad)
                if let error = viewModel.priceError {
                    Text(error).font(.caption).foregroundStyle(.red)
                }
            }
            
            Section {
                AppButton("Create Product", isLoading: viewModel.isSubmitting) {
                    focusedField = nil
                    Task { await viewModel.submit() }
                }
                .disabled(!viewModel.isFormValid)
            }
        }
        .navigationTitle("New Product")
        .onChange(of: viewModel.didSubmitSuccessfully) { _, success in
            if success { dismiss() }
        }
    }
}
```

---

## Performance Guidelines

### View Performance

1. **Use `LazyVStack` / `LazyHStack`** for any list with 10+ items — never `VStack` with `ForEach` for large collections
2. **Extract subviews** — SwiftUI diffs per-view; smaller views = fewer recomputations
3. **Use `@Observable` granularly** — properties track individually; only views reading changed properties re-render
4. **Mark child views `Equa

…(truncated)
