Skip to content

Latest commit

 

History

History
722 lines (546 loc) · 18.1 KB

File metadata and controls

722 lines (546 loc) · 18.1 KB

VIPER Architecture Guide

Overview

VIPER is a highly modular iOS architecture pattern that provides maximum separation of concerns. The name is an acronym for its five components:

  • View: Displays data and forwards user actions
  • Interactor: Contains ALL business logic
  • Presenter: Formats data for display and handles view logic
  • Entity: Pure data models
  • Router: Handles navigation and module coordination

Architecture Diagram

graph TB
    subgraph VIPER["VIPER Architecture"]
        View["VIEW<br/>(SwiftUI)<br/>Displays data<br/>Forwards actions"]
        Presenter["PRESENTER<br/>(Formats Data)<br/>Presentation logic<br/>Coordinates"]
        Interactor["INTERACTOR<br/>(Business Logic)<br/>Data operations<br/>Business rules"]
        Entity["ENTITIES<br/>(Data Models)<br/>Pure data structures"]
        Router["ROUTER<br/>(Navigation)<br/>Wires modules<br/>Manages state"]

        View -->|"User Actions"| Presenter
        Presenter -->|"Display Data"| View
        Presenter -->|"Business Logic Calls"| Interactor
        Interactor -->|"Observable Changes"| Presenter
        Interactor -->|"Reads/Writes"| Entity
        Presenter -->|"Navigation Requests"| Router
        Router -->|"Creates Modules"| Presenter
        Router -->|"Navigation State"| View
    end

    style View fill:#e3f2fd
    style Presenter fill:#fff3e0
    style Interactor fill:#f3e5f5
    style Entity fill:#e8f5e9
    style Router fill:#fce4ec
Loading

Component Responsibilities

View (V)

Completely dumb - no logic whatsoever.

Responsibilities

  • ✅ Display data provided by Presenter
  • ✅ Forward user actions to Presenter
  • ✅ Update when Presenter's observable properties change

What Views Should NOT Do

  • ❌ Contain business logic
  • ❌ Format data for display
  • ❌ Make navigation decisions
  • ❌ Directly communicate with Interactor
  • ❌ Know about Entities

Example

struct VIPERSidebarView: View {
    @Binding var selectedCategory: VIPERSidebarCategory?
    var presenter: VIPERSidebarPresenter

    var body: some View {
        List(presenter.categories, selection: $selectedCategory) { category in
            NavigationLink(value: category) {
                HStack {
                    // Get formatted data from Presenter
                    Image(systemName: presenter.iconName(for: category))
                    Text(category.rawValue)
                }
            }
        }
        .onChange(of: selectedCategory) { _, newValue in
            if let newValue {
                // Forward user action to Presenter
                presenter.selectCategory(newValue)
            }
        }
    }
}

Interactor (I)

Contains ALL business logic - the "brain" of the module.

Responsibilities

  • ✅ Implement business rules and validation
  • ✅ Fetch data (from network, database, cache)
  • ✅ Create, update, delete Entities
  • ✅ Coordinate with other services (networking, persistence, etc.)
  • ✅ Notify Presenter of data changes via @Observable

What Interactors Should NOT Do

  • ❌ Know about Views or UI
  • ❌ Format data for display
  • ❌ Handle navigation
  • ❌ Directly communicate with Views

Example

@Observable
@MainActor
class VIPERContentListInteractor {
    private var itemsByCategory: [VIPERSidebarCategory: [VIPERListItem]] = [:]

    func fetchItems(for category: VIPERSidebarCategory) -> [VIPERListItem] {
        return itemsByCategory[category] ?? []
    }

    func addItem(title: String, subtitle: String, to category: VIPERSidebarCategory) {
        // Business logic: Create new item
        let newItem = VIPERListItem(title: title, subtitle: subtitle)

        // Business logic: Add to appropriate category
        var items = itemsByCategory[category] ?? []
        items.insert(newItem, at: 0)
        itemsByCategory[category] = items

        // In production: Persist, sync, validate, etc.
    }
}

Presenter (P)

Formats data and handles view logic - the "translator" between View and Interactor.

Responsibilities

  • ✅ Format data from Interactor for View display
  • ✅ Handle user actions from View
  • ✅ Tell Interactor what business logic to execute
  • ✅ Tell Router where to navigate
  • ✅ Manage view state (loading, error, empty states)

What Presenters Should NOT Do

  • ❌ Contain business logic (delegate to Interactor)
  • ❌ Directly manipulate Entities (ask Interactor)
  • ❌ Know about SwiftUI or UIKit specifics
  • ❌ Perform navigation (delegate to Router)

Example

@Observable
@MainActor
class VIPERContentListPresenter {
    private let interactor: VIPERContentListInteractor
    private let router: VIPERRouter

    func items(for category: VIPERSidebarCategory) -> [VIPERListItem] {
        // Get data from Interactor
        let rawItems = interactor.fetchItems(for: category)

        // Could format here (uppercase titles, add badges, etc.)
        return rawItems
    }

    func addItem(title: String, subtitle: String, to category: VIPERSidebarCategory) {
        // Could validate here (view logic)
        guard !title.isEmpty else { return }

        // Delegate business logic to Interactor
        interactor.addItem(title: title, subtitle: subtitle, to: category)
    }

    func selectItem(_ item: VIPERListItem) {
        // Delegate navigation to Router
        router.didSelectItem(item)
    }
}

Entity (E)

Pure data models - no logic whatsoever.

Responsibilities

  • ✅ Define data structures
  • ✅ Conform to protocols (Identifiable, Hashable, Codable)

What Entities Should NOT Do

  • ❌ Contain business logic
  • ❌ Know about other components
  • ❌ Perform validation (Interactor's job)
  • ❌ Format data (Presenter's job)

Example

struct VIPERListItem: Identifiable, Hashable {
    let id = UUID()
    let title: String
    let subtitle: String
    let status: String
    let createdDate: String
    let modifiedDate: String
}

Router (R)

Handles navigation and module coordination - the "assembly line".

Responsibilities

  • ✅ Create VIPER modules with all dependencies
  • ✅ Wire up View-Interactor-Presenter relationships
  • ✅ Manage navigation state
  • ✅ Coordinate between different modules
  • ✅ Handle deep linking and external navigation

What Routers Should NOT Do

  • ❌ Contain business logic
  • ❌ Format data for display
  • ❌ Directly manipulate Entities

Example

@Observable
@MainActor
class VIPERRouter {
    var selectedCategory: VIPERSidebarCategory? = .category1
    var selectedItem: VIPERListItem? = nil

    let sidebarInteractor = VIPERSidebarInteractor()
    let contentListInteractor = VIPERContentListInteractor()

    func makeSidebarPresenter() -> VIPERSidebarPresenter {
        VIPERSidebarPresenter(
            interactor: sidebarInteractor,
            router: self
        )
    }

    func makeContentListPresenter() -> VIPERContentListPresenter {
        VIPERContentListPresenter(
            interactor: contentListInteractor,
            router: self
        )
    }

    func didSelectCategory(_ category: VIPERSidebarCategory) {
        selectedCategory = category
        selectedItem = nil  // Clear item selection
    }
}

Data Flow Examples

User Taps Add Button

sequenceDiagram
    participant User
    participant View
    participant Presenter
    participant Interactor
    participant Entity

    User->>View: Taps "Add" button
    View->>Presenter: addItem(title: "New Item", ...)
    Note over Presenter: Could validate input
    Presenter->>Interactor: addItem(title: "New Item", ...)
    Interactor->>Entity: Create VIPERListItem
    Note over Interactor: Add to storage
    Interactor-->>Presenter: @Observable notification
    Presenter-->>View: @Observable update
    View->>View: Re-render list
Loading

User Selects Item

sequenceDiagram
    participant User
    participant View
    participant Presenter
    participant Router
    participant DetailView

    User->>View: Taps item in list
    View->>Presenter: selectItem(item)
    Presenter->>Router: didSelectItem(item)
    Router->>Router: Set selectedItem = item
    Router-->>DetailView: @Observable update
    DetailView->>DetailView: Show item details
Loading

Fetching Data

sequenceDiagram
    participant View
    participant Presenter
    participant Interactor
    participant Entity

    View->>Presenter: items(for: category)
    Presenter->>Interactor: fetchItems(for: category)
    Note over Interactor: Apply business rules<br/>(filtering, sorting)
    Interactor->>Entity: Retrieve data
    Entity-->>Interactor: Raw data
    Interactor-->>Presenter: [VIPERListItem]
    Note over Presenter: Could format data<br/>(uppercase, badges)
    Presenter-->>View: [VIPERListItem]
    View->>View: Display in List
Loading

When to Use VIPER

✅ Use VIPER When

  1. Large, Complex Applications

    • Multiple features and modules
    • Complex business logic
    • Many navigation flows
  2. Team Development

    • Multiple developers working together
    • Need clear separation of responsibilities
    • Want to avoid merge conflicts
  3. Maximum Testability

    • Need to test all layers independently
    • Want to mock dependencies easily
    • Require high test coverage
  4. Long-Term Maintainability

    • App will be maintained for years
    • Code needs to be easy to understand and modify
    • Team members will change over time
  5. Complex Business Rules

    • Heavy data processing
    • Complex validation logic
    • Multiple data sources

❌ Don't Use VIPER When

  1. Simple Applications

    • Prototypes or MVPs
    • Few screens with simple logic
    • Short development timeline
  2. Small Teams

    • Solo developer or small team
    • Rapid iteration needed
    • Architecture overhead not justified
  3. Learning SwiftUI

    • Just starting with SwiftUI
    • Want to focus on UI concepts
    • VIPER adds too much complexity
  4. Time Constraints

    • Tight deadlines
    • Need to ship quickly
    • Can't afford setup overhead

VIPER vs. MVVM vs. ViewOnly

ViewOnly Pattern

Simplest: State lives in Views

struct ContentView: View {
    @State private var selectedCategory: Category? = .category1
    @State private var selectedItem: ListItem? = nil

    var body: some View {
        // Views pass state via @Binding
    }
}

Pros:

  • ✅ Simplest to understand
  • ✅ Least code
  • ✅ Fast to develop
  • ✅ Great for prototypes

Cons:

  • ❌ State scattered across Views
  • ❌ Hard to test
  • ❌ Doesn't scale well
  • ❌ Business logic mixed with UI

When to Use: Prototypes, learning SwiftUI, very simple apps


MVVM Pattern

Moderate: ViewModels manage state and logic

@Observable
class AppViewModel {
    var selectedCategory: Category? = .category1
    var selectedItem: ListItem? = nil

    let contentListViewModel = ContentListViewModel()
    let settingsViewModel = SettingsViewModel()

    func addItem(to category: Category) {
        // Business logic AND presentation logic mixed
    }
}

Pros:

  • ✅ Good separation from UI
  • ✅ Testable
  • ✅ Moderate complexity
  • ✅ SwiftUI-friendly

Cons:

  • ❌ ViewModels can become bloated
  • ❌ Business and presentation logic mixed
  • ❌ Less separation than VIPER
  • ❌ Can be hard to test business logic

When to Use: Most production apps, moderate complexity, team development


VIPER Pattern

Maximum: Five separate components

// Interactor: Business logic ONLY
class ContentListInteractor {
    func addItem(title: String, to category: Category) {
        // ONLY business logic
    }
}

// Presenter: Presentation logic ONLY
class ContentListPresenter {
    func addItem(title: String, to category: Category) {
        // View validation, then delegate to Interactor
        interactor.addItem(title: title, to: category)
    }
}

// Router: Navigation ONLY
class Router {
    func didSelectItem(_ item: ListItem) {
        selectedItem = item
    }
}

Pros:

  • ✅ Maximum separation of concerns
  • ✅ Highly testable
  • ✅ Clear responsibilities
  • ✅ Scales to large apps
  • ✅ Great for teams

Cons:

  • ❌ Most complex
  • ❌ Most boilerplate
  • ❌ Slower initial development
  • ❌ Can be overkill for simple apps

When to Use: Large apps, complex business logic, large teams, long-term projects


Comparison Table

Feature ViewOnly MVVM VIPER
Complexity Low Medium High
Boilerplate Minimal Moderate Significant
Testability Poor Good Excellent
Separation Minimal Good Maximum
Learning Curve Easy Moderate Steep
Team Size 1-2 2-10 5+
App Complexity Simple Moderate Complex
Development Speed Fast Moderate Slow
Maintainability Poor Good Excellent

Project Structure

Examples/VIPER/
├── Entities/                      # Pure data models
│   ├── VIPERListItem.swift
│   └── VIPERSidebarCategory.swift
│
├── Interactors/                   # Business logic
│   ├── VIPERContentListInteractor.swift
│   ├── VIPERSidebarInteractor.swift
│   └── VIPERSettingsInteractor.swift
│
├── Presenters/                    # Presentation logic
│   ├── VIPERContentListPresenter.swift
│   ├── VIPERSidebarPresenter.swift
│   ├── VIPERDetailPresenter.swift
│   └── VIPERSettingsPresenter.swift
│
├── Router/                        # Navigation
│   └── VIPERRouter.swift
│
└── Views/                         # SwiftUI views
    ├── VIPERContentView.swift
    ├── VIPERSidebarView.swift
    ├── VIPERContentListView.swift
    ├── VIPERDetailView.swift
    ├── VIPERSettingsContentView.swift
    └── ... (detail tabs, etc.)

Testing Strategy

Unit Testing Interactors

class ContentListInteractorTests: XCTestCase {
    func testAddItem() {
        let interactor = VIPERContentListInteractor()

        interactor.addItem(
            title: "Test Item",
            subtitle: "Test Subtitle",
            to: .category1
        )

        let items = interactor.fetchItems(for: .category1)
        XCTAssertEqual(items.count, 1)
        XCTAssertEqual(items.first?.title, "Test Item")
    }
}

Unit Testing Presenters

class ContentListPresenterTests: XCTestCase {
    func testAddItem() {
        let mockInteractor = MockContentListInteractor()
        let mockRouter = MockRouter()
        let presenter = VIPERContentListPresenter(
            interactor: mockInteractor,
            router: mockRouter
        )

        presenter.addItem(
            title: "Test",
            subtitle: "Test",
            to: .category1
        )

        XCTAssertTrue(mockInteractor.addItemWasCalled)
    }
}

SwiftUI Integration

VIPER predates SwiftUI, so we adapt it using modern SwiftUI features:

Use @Observable Instead of Protocols

Traditional VIPER (Pre-SwiftUI):

protocol ContentListPresenterProtocol {
    func fetchItems() -> [Item]
}

protocol ContentListViewProtocol: AnyObject {
    func displayItems(_ items: [Item])
}

Modern VIPER (SwiftUI):

@Observable
@MainActor
class ContentListPresenter {
    func items(for category: Category) -> [Item] {
        // SwiftUI automatically observes changes
        return interactor.fetchItems(for: category)
    }
}

Router Creates SwiftUI Views

class VIPERRouter {
    @ViewBuilder
    func makeContentListView(for category: Category) -> some View {
        VIPERContentListView(
            category: category,
            presenter: makeContentListPresenter()
        )
    }
}

Best Practices

  1. Keep Views Completely Dumb

    • Views should only display and forward actions
    • No business logic, no formatting
  2. Use Dependency Injection

    • Inject Interactors into Presenters
    • Inject Router into Presenters
    • Enables testing and flexibility
  3. Single Responsibility

    • Each component does ONE thing
    • Interactor = business logic
    • Presenter = presentation logic
    • Router = navigation
  4. Protocol-Oriented (Optional)

    • Define protocols for each component
    • Enables mocking for tests
    • More boilerplate but more testable
  5. Router Owns Lifecycle

    • Router creates all components
    • Router wires dependencies
    • Router is single source of truth

Common Pitfalls

  1. Too Much Boilerplate

    • Don't create protocols for everything
    • Use @Observable for automatic updates
    • Keep it practical
  2. Confusion About Presenter vs. Interactor

    • Presenter = HOW to display
    • Interactor = WHAT to do
    • If it affects business logic → Interactor
    • If it's only for display → Presenter
  3. Over-Engineering Simple Views

    • Not every view needs full VIPER
    • Simple detail views can just read from Router
    • Use judgment
  4. Circular Dependencies

    • View → Presenter → Interactor ✅
    • Interactor → Presenter ❌
    • Use @Observable for automatic updates

Learning Path

  1. Start with ViewOnly

    • Understand basic SwiftUI state
    • Learn @State and @Binding
  2. Move to MVVM

    • Understand ViewModels
    • Learn @Observable macro
    • Separate UI from logic
  3. Then Learn VIPER

    • Understand why more separation helps
    • Learn when to use each component
    • Practice with small modules first

Resources

Conclusion

VIPER provides maximum separation of concerns and testability, but comes with significant complexity. Use it for large, complex applications with multiple developers. For smaller projects, MVVM or even ViewOnly may be more appropriate.

The key is understanding the trade-offs and choosing the right pattern for your specific needs.