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
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
Completely dumb - no logic whatsoever.
- ✅ Display data provided by Presenter
- ✅ Forward user actions to Presenter
- ✅ Update when Presenter's observable properties change
- ❌ Contain business logic
- ❌ Format data for display
- ❌ Make navigation decisions
- ❌ Directly communicate with Interactor
- ❌ Know about Entities
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)
}
}
}
}Contains ALL business logic - the "brain" of the module.
- ✅ 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
- ❌ Know about Views or UI
- ❌ Format data for display
- ❌ Handle navigation
- ❌ Directly communicate with Views
@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.
}
}Formats data and handles view logic - the "translator" between View and Interactor.
- ✅ 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)
- ❌ Contain business logic (delegate to Interactor)
- ❌ Directly manipulate Entities (ask Interactor)
- ❌ Know about SwiftUI or UIKit specifics
- ❌ Perform navigation (delegate to Router)
@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)
}
}Pure data models - no logic whatsoever.
- ✅ Define data structures
- ✅ Conform to protocols (Identifiable, Hashable, Codable)
- ❌ Contain business logic
- ❌ Know about other components
- ❌ Perform validation (Interactor's job)
- ❌ Format data (Presenter's job)
struct VIPERListItem: Identifiable, Hashable {
let id = UUID()
let title: String
let subtitle: String
let status: String
let createdDate: String
let modifiedDate: String
}Handles navigation and module coordination - the "assembly line".
- ✅ 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
- ❌ Contain business logic
- ❌ Format data for display
- ❌ Directly manipulate Entities
@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
}
}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
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
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
-
Large, Complex Applications
- Multiple features and modules
- Complex business logic
- Many navigation flows
-
Team Development
- Multiple developers working together
- Need clear separation of responsibilities
- Want to avoid merge conflicts
-
Maximum Testability
- Need to test all layers independently
- Want to mock dependencies easily
- Require high test coverage
-
Long-Term Maintainability
- App will be maintained for years
- Code needs to be easy to understand and modify
- Team members will change over time
-
Complex Business Rules
- Heavy data processing
- Complex validation logic
- Multiple data sources
-
Simple Applications
- Prototypes or MVPs
- Few screens with simple logic
- Short development timeline
-
Small Teams
- Solo developer or small team
- Rapid iteration needed
- Architecture overhead not justified
-
Learning SwiftUI
- Just starting with SwiftUI
- Want to focus on UI concepts
- VIPER adds too much complexity
-
Time Constraints
- Tight deadlines
- Need to ship quickly
- Can't afford setup overhead
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
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
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
| 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 |
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.)
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")
}
}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)
}
}VIPER predates SwiftUI, so we adapt it using modern SwiftUI features:
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)
}
}class VIPERRouter {
@ViewBuilder
func makeContentListView(for category: Category) -> some View {
VIPERContentListView(
category: category,
presenter: makeContentListPresenter()
)
}
}-
Keep Views Completely Dumb
- Views should only display and forward actions
- No business logic, no formatting
-
Use Dependency Injection
- Inject Interactors into Presenters
- Inject Router into Presenters
- Enables testing and flexibility
-
Single Responsibility
- Each component does ONE thing
- Interactor = business logic
- Presenter = presentation logic
- Router = navigation
-
Protocol-Oriented (Optional)
- Define protocols for each component
- Enables mocking for tests
- More boilerplate but more testable
-
Router Owns Lifecycle
- Router creates all components
- Router wires dependencies
- Router is single source of truth
-
Too Much Boilerplate
- Don't create protocols for everything
- Use @Observable for automatic updates
- Keep it practical
-
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
-
Over-Engineering Simple Views
- Not every view needs full VIPER
- Simple detail views can just read from Router
- Use judgment
-
Circular Dependencies
- View → Presenter → Interactor ✅
- Interactor → Presenter ❌
- Use @Observable for automatic updates
-
Start with ViewOnly
- Understand basic SwiftUI state
- Learn @State and @Binding
-
Move to MVVM
- Understand ViewModels
- Learn @Observable macro
- Separate UI from logic
-
Then Learn VIPER
- Understand why more separation helps
- Learn when to use each component
- Practice with small modules first
- Martin Fowler on Presentation Patterns
- objc.io App Architecture Book
- Clean Architecture by Uncle Bob
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.