mvvm-architecture

Expert MVVM decisions for iOS/tvOS: choosing between ViewModel patterns (state enum vs published properties vs Combine), service layer boundaries, dependency injection strategies, and testing approaches. Use when designing ViewModel architecture, debugging data flow issues, or deciding where business logic belongs. Trigger keywords: MVVM, ViewModel, ObservableObject, @StateObject, service layer, dependency injection, unit test, mock, architecture

16 stars

Best use case

mvvm-architecture is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Expert MVVM decisions for iOS/tvOS: choosing between ViewModel patterns (state enum vs published properties vs Combine), service layer boundaries, dependency injection strategies, and testing approaches. Use when designing ViewModel architecture, debugging data flow issues, or deciding where business logic belongs. Trigger keywords: MVVM, ViewModel, ObservableObject, @StateObject, service layer, dependency injection, unit test, mock, architecture

Teams using mvvm-architecture should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/mvvm-architecture/SKILL.md --create-dirs "https://raw.githubusercontent.com/diegosouzapw/awesome-omni-skill/main/skills/data-ai/mvvm-architecture/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/mvvm-architecture/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How mvvm-architecture Compares

Feature / Agentmvvm-architectureStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Expert MVVM decisions for iOS/tvOS: choosing between ViewModel patterns (state enum vs published properties vs Combine), service layer boundaries, dependency injection strategies, and testing approaches. Use when designing ViewModel architecture, debugging data flow issues, or deciding where business logic belongs. Trigger keywords: MVVM, ViewModel, ObservableObject, @StateObject, service layer, dependency injection, unit test, mock, architecture

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# MVVM Architecture — Expert Decisions

Expert decision frameworks for MVVM choices in iOS/tvOS. Claude knows MVVM basics — this skill provides judgment calls for non-obvious decisions.

---

## Decision Trees

### ViewModel Pattern Selection

```
Does the screen have distinct, mutually exclusive states?
├─ YES (loading → loaded → error)
│  └─ State Enum Pattern
│     @Published var state: State = .idle
│     enum State { case idle, loading, loaded(Data), error(String) }
│
└─ NO (multiple independent properties)
   └─ Does the screen need form validation?
      ├─ YES → Combine Pattern (publishers for validation chains)
      └─ NO → Published Properties Pattern (simplest)
```

**When State Enum wins**: Product detail (loading → product → error), authentication flows, wizard steps. Forces exhaustive handling.

**When Published Properties win**: Dashboard with multiple independent sections that load/fail independently. State enum becomes unwieldy with 2^n combinations.

### Where Does Logic Belong?

```
Is it data transformation for display?
├─ YES → ViewModel (formatting, filtering visible data)
│
└─ NO → Is it reusable business logic?
   ├─ YES → Service Layer (API calls, validation rules, caching)
   │
   └─ NO → Is it pure domain logic?
      ├─ YES → Model (computed properties, domain rules)
      └─ NO → Reconsider if it's needed
```

**The trap**: Putting API calls directly in ViewModel. Makes testing require network mocking instead of simple service mocking.

### @StateObject Injection

```
Does ViewModel need dependencies from parent?
├─ NO → Direct initialization
│  @StateObject private var viewModel = UserViewModel()
│
└─ YES → How many dependencies?
   ├─ 1-2 → Init parameter
   │  init(userId: String) {
   │      _viewModel = StateObject(wrappedValue: UserViewModel(userId: userId))
   │  }
   │
   └─ Many → Factory/Container
      @StateObject private var viewModel: UserViewModel
      init() {
          _viewModel = StateObject(wrappedValue: Container.shared.makeUserViewModel())
      }
```

---

## NEVER Do

### ViewModel Anti-Patterns

**NEVER** load data in ViewModel `init`:
```swift
// ❌ Starts loading before view appears, can't cancel, can't retry
class BadViewModel: ObservableObject {
    init() {
        Task { await loadData() }  // Fire-and-forget in init
    }
}

// ✅ Load via .task modifier — automatic cancellation on disappear
struct GoodView: View {
    @StateObject var viewModel = GoodViewModel()
    var body: some View {
        content.task { await viewModel.loadData() }
    }
}
```

**NEVER** expose mutable state directly:
```swift
// ❌ Anyone can mutate — no control over state transitions
class BadViewModel: ObservableObject {
    @Published var users: [User] = []  // Public setter
}

// ✅ private(set) — only ViewModel controls mutations
class GoodViewModel: ObservableObject {
    @Published private(set) var users: [User] = []

    func addUser(_ user: User) {
        // Validation, analytics, etc.
        users.append(user)
    }
}
```

**NEVER** put UI-specific code in ViewModel:
```swift
// ❌ ViewModel knows about colors, fonts, formatters
class BadViewModel: ObservableObject {
    @Published var priceColor: Color = .green
    @Published var formattedDate: String = ""  // Pre-formatted for display
}

// ✅ Return data, let View handle presentation
class GoodViewModel: ObservableObject {
    @Published private(set) var price: Decimal = 0
    @Published private(set) var date: Date = .now
}
// View: Text(viewModel.price, format: .currency(code: "USD"))
```

**NEVER** create god ViewModels:
```swift
// ❌ One ViewModel for entire feature area
class UserViewModel: ObservableObject {
    // Profile, settings, posts, friends, notifications, activity...
    // 50+ @Published properties, 30+ methods
}

// ✅ One ViewModel per screen/concern
class UserProfileViewModel: ObservableObject { }
class UserSettingsViewModel: ObservableObject { }
class UserPostsViewModel: ObservableObject { }
```

### Service Layer Anti-Patterns

**NEVER** use concrete dependencies:
```swift
// ❌ Hard to test — must mock URLSession
class BadViewModel: ObservableObject {
    func loadUsers() async {
        let url = URL(string: "https://api.example.com/users")!
        let (data, _) = try await URLSession.shared.data(from: url)  // Concrete
    }
}

// ✅ Protocol dependency — inject mock for testing
protocol UserServiceProtocol {
    func fetchUsers() async throws -> [User]
}

class GoodViewModel: ObservableObject {
    private let userService: UserServiceProtocol

    init(userService: UserServiceProtocol = UserService()) {
        self.userService = userService
    }
}
```

**NEVER** ignore task cancellation:
```swift
// ❌ Shows error for cancelled task (user navigated away)
func loadData() async {
    do {
        users = try await service.fetchUsers()
    } catch {
        errorMessage = error.localizedDescription  // CancellationError shows error!
    }
}

// ✅ Handle cancellation separately
func loadData() async {
    do {
        users = try await service.fetchUsers()
    } catch is CancellationError {
        return  // User navigated away — don't show error
    } catch {
        errorMessage = error.localizedDescription
    }
}
```

---

## Core Patterns

### Minimal ViewModel Template

```swift
@MainActor
final class FeatureViewModel: ObservableObject {
    // MARK: - State
    @Published private(set) var items: [Item] = []
    @Published private(set) var isLoading = false
    @Published var error: Error?

    // MARK: - Dependencies
    private let service: ServiceProtocol

    init(service: ServiceProtocol = Service()) {
        self.service = service
    }

    // MARK: - Actions
    func loadItems() async {
        isLoading = true
        defer { isLoading = false }

        do {
            items = try await service.fetchItems()
        } catch is CancellationError {
            return
        } catch {
            self.error = error
        }
    }
}
```

### State Enum Pattern

```swift
@MainActor
final class DetailViewModel: ObservableObject {
    enum State: Equatable {
        case idle
        case loading
        case loaded(Item)
        case error(String)

        var item: Item? {
            guard case .loaded(let item) = self else { return nil }
            return item
        }
    }

    @Published private(set) var state: State = .idle

    func load(id: String) async {
        state = .loading
        do {
            let item = try await service.fetch(id: id)
            state = .loaded(item)
        } catch {
            state = .error(error.localizedDescription)
        }
    }
}

// View exhaustive handling
switch viewModel.state {
case .idle, .loading: ProgressView()
case .loaded(let item): ItemView(item: item)
case .error(let message): ErrorView(message: message)
}
```

### Service Protocol Pattern

```swift
// Protocol — the contract
protocol UserServiceProtocol {
    func fetchUser(id: String) async throws -> User
    func updateUser(_ user: User) async throws -> User
}

// Real implementation
final class UserService: UserServiceProtocol {
    private let client: NetworkClient

    func fetchUser(id: String) async throws -> User {
        try await client.request(.user(id: id))
    }
}

// Mock for testing
final class MockUserService: UserServiceProtocol {
    var stubbedUser: User?
    var fetchError: Error?
    var fetchCallCount = 0

    func fetchUser(id: String) async throws -> User {
        fetchCallCount += 1
        if let error = fetchError { throw error }
        return stubbedUser ?? User.mock()
    }
}
```

---

## Testing Strategy

### ViewModel Test Structure

```swift
@MainActor
final class UserViewModelTests: XCTestCase {
    var sut: UserViewModel!
    var mockService: MockUserService!

    override func setUp() {
        mockService = MockUserService()
        sut = UserViewModel(service: mockService)
    }

    func test_loadUser_success_updatesState() async {
        // Given
        mockService.stubbedUser = User.mock(name: "John")

        // When
        await sut.loadUser(id: "123")

        // Then
        XCTAssertEqual(sut.user?.name, "John")
        XCTAssertFalse(sut.isLoading)
        XCTAssertNil(sut.error)
    }

    func test_loadUser_failure_setsError() async {
        // Given
        mockService.fetchError = NetworkError.noConnection

        // When
        await sut.loadUser(id: "123")

        // Then
        XCTAssertNil(sut.user)
        XCTAssertNotNil(sut.error)
    }
}
```

**Test what matters**:
1. State changes on success/failure
2. Service method called with correct parameters
3. Loading states transition correctly
4. Error handling doesn't crash

**Don't test**:
- SwiftUI bindings (Apple's responsibility)
- Service implementation (separate test file)

---

## Dependency Injection

### Simple: Default Parameters

```swift
// Most apps need nothing more complex
class UserViewModel: ObservableObject {
    init(service: UserServiceProtocol = UserService()) {
        self.service = service
    }
}

// Test: UserViewModel(service: MockUserService())
// Production: UserViewModel() — uses default
```

### Complex: Factory Container

```swift
// Only when you have many cross-cutting dependencies
@MainActor
final class Container {
    static let shared = Container()

    lazy var networkClient = NetworkClient()
    lazy var authService = AuthService(client: networkClient)
    lazy var userService = UserService(client: networkClient, auth: authService)

    func makeUserViewModel() -> UserViewModel {
        UserViewModel(service: userService)
    }
}
```

---

## Quick Reference

### Layer Responsibilities

| Layer | Contains | Examples |
|-------|----------|----------|
| **Model** | Domain data + pure logic | User, Order, validation rules |
| **ViewModel** | Screen state + UI logic | Loading/error states, list filtering |
| **Service** | Business operations | API calls, caching, persistence |
| **View** | Presentation | Layout, styling, animations |

### ViewModel Checklist

- [ ] `@MainActor` on class
- [ ] `private(set)` on @Published properties
- [ ] Protocol-based dependencies with defaults
- [ ] CancellationError handled separately
- [ ] No UI types (Color, Font, etc.)
- [ ] No direct network/database calls
- [ ] Testable without UI framework

Related Skills

MCP Architecture Expert

16
from diegosouzapw/awesome-omni-skill

Design and implement Model Context Protocol servers for standardized AI-to-data integration with resources, tools, prompts, and security best practices

architecture-paradigm-pipeline

16
from diegosouzapw/awesome-omni-skill

Consult this skill when designing data pipelines or transformation workflows. Use when data flows through fixed sequence of transformations, stages can be independently developed and tested, parallel processing of stages is beneficial. Do not use when selecting from multiple paradigms - use architecture-paradigms first. DO NOT use when: data flow is not sequential or predictable. DO NOT use when: complex branching/merging logic dominates.

architecture-advisor

16
from diegosouzapw/awesome-omni-skill

Helps solo developers with AI agents choose optimal architecture (monolithic/microservices/hybrid)

agent-native-architecture

16
from diegosouzapw/awesome-omni-skill

Build applications where agents are first-class citizens. Use this skill when designing autonomous agents, creating MCP tools, implementing self-modifying systems, or building apps where features are outcomes achieved by agents operating in a loop.

agent-architecture

16
from diegosouzapw/awesome-omni-skill

Use when designing or implementing AI agent systems. Covers tool-using agents with mandatory guardrails, SSE streaming (FastAPI → Next.js via Vercel AI SDK v6), LangGraph stateful multi-agent graphs, episodic memory via pgvector, MCP overview, and production failure modes with anti-pattern/fix code pairs.

u07820-attention-management-architecture-for-personal-finance-management

16
from diegosouzapw/awesome-omni-skill

Build and operate the "Attention Management Architecture for personal finance management" capability for personal finance management. Use when this exact capability is required by autonomous or human-guided missions.

MCP Server Architecture

16
from diegosouzapw/awesome-omni-skill

This skill should be used when the user asks to "create an MCP server", "set up MCP server", "build ChatGPT app backend", "MCP transport type", "configure MCP endpoint", "server setup for Apps SDK", or needs guidance on MCP server architecture, transport protocols, or SDK setup for the OpenAI Apps SDK.

architecture-discipline

16
from diegosouzapw/awesome-omni-skill

Use when designing/modifying system architecture or evaluating technology choices. Enforces 7-section TodoWrite with 22+ items. Triggers: "design architecture", "system design", "architectural decision", "should we use [tech]", "compare [A] vs [B]", "add new service", "microservices", "database choice", "API design", "scale to [X] users", "infrastructure decision". If thinking ANY of these, USE THIS SKILL: "quick recommendation is fine", "obvious choice", "we already know the answer", "just need to pick one", "simple architecture question".

api-tier-architecture

16
from diegosouzapw/awesome-omni-skill

3-tier API architecture (Convex WebSocket, SSE, REST) for cross-platform data fetching. Platform detection, hybrid hooks, DAL layer patterns. Triggers on "API", "tier", "Convex", "REST", "SSE", "useConvexQuery", "useQuery", "withAuth", "DAL".

adr-architecture

16
from diegosouzapw/awesome-omni-skill

Use when documenting significant technical or architectural decisions that need context, rationale, and consequences recorded. Invoke when choosing between technology options, making infrastructure decisions, establishing standards, migrating systems, or when team needs to understand why a decision was made. Use when user mentions ADR, architecture decision, technical decision record, or decision documentation.

langchain-architecture

16
from diegosouzapw/awesome-omni-skill

Design LLM applications using the LangChain framework with agents, memory, and tool integration patterns. Use when building LangChain applications, implementing AI agents, or creating complex LLM w...

architecture-agent-creation

16
from diegosouzapw/awesome-omni-skill

Create specialized infrastructure agent definitions for platform/service management (Grafana, Prometheus, Traefik, ERPNext, etc.). Use when the user requests creation of an agent for a specific technology platform or infrastructure component. This skill produces complete agent prompts with integrated research, SOPs, tool references, and handoff protocols following the Linear-First Agentic Workflow framework.