Agent Skills: VIPER Architecture (Rambler Team)

Use when architecting complex iOS apps with multiple features, long-term maintenance requirements, or team scalability needs. Use when refactoring Massive View Controllers or implementing testable architecture. Do NOT use for simple single-screen apps, rapid prototypes, or small utility tools.

UncategorizedID: dagba/ios-mcp/viper-architecture-rambler

Install this agent skill to your local

pnpm dlx add-skill https://github.com/dagba/ios-mcp/tree/HEAD/skills/viper-architecture-rambler

Skill Files

Browse the full folder contents for viper-architecture-rambler.

Download Skill

Loading file tree…

skills/viper-architecture-rambler/SKILL.md

Skill Metadata

Name
viper-architecture-rambler
Description
Use when architecting complex iOS apps with multiple features, long-term maintenance requirements, or team scalability needs. Use when refactoring Massive View Controllers or implementing testable architecture. Do NOT use for simple single-screen apps, rapid prototypes, or small utility tools.

VIPER Architecture (Rambler Team)

Overview

VIPER enforces Single Responsibility Principle through five protocol-based components: View, Interactor, Presenter, Entity, Router. Created and documented by Rambler&Co iOS Team.

Core Principle: Entities never reach the Presentation layer. Simple data structures transfer between Interactor and Presenter, preventing business logic pollution in UI.

When to Use VIPER

digraph viper_decision {
    "Complex app?" [shape=diamond];
    "Multiple features?" [shape=diamond];
    "Long-term project?" [shape=diamond];
    "Team size > 2?" [shape=diamond];
    "High testability need?" [shape=diamond];
    "Use VIPER" [shape=box, style=filled, fillcolor=lightgreen];
    "Use MVC/MVVM" [shape=box, style=filled, fillcolor=lightcoral];

    "Complex app?" -> "Multiple features?" [label="yes"];
    "Complex app?" -> "Use MVC/MVVM" [label="no"];
    "Multiple features?" -> "Long-term project?" [label="yes"];
    "Multiple features?" -> "Use MVC/MVVM" [label="no"];
    "Long-term project?" -> "Team size > 2?" [label="yes"];
    "Long-term project?" -> "High testability need?" [label="no"];
    "Team size > 2?" -> "Use VIPER" [label="yes"];
    "Team size > 2?" -> "High testability need?" [label="no"];
    "High testability need?" -> "Use VIPER" [label="yes"];
    "High testability need?" -> "Use MVC/MVVM" [label="no"];
}

Use VIPER for:

  • Multi-feature apps (authentication, payments, user profiles, etc.)
  • Long-term projects (6+ months)
  • Teams with 3+ developers
  • Apps requiring extensive unit test coverage
  • Multi-platform code sharing (iOS/iPadOS/macOS)

Avoid VIPER for:

  • Single-screen utilities
  • Rapid prototypes with unclear requirements
  • Weekend projects or proof-of-concepts
  • Small teams with tight deadlines

Component Responsibilities

| Component | Responsibility | What It Must NOT Do | |-----------|---------------|---------------------| | View | Display content, relay user input (passive) | Request data, format data, navigation, business logic | | Interactor | Business logic, use cases (platform-independent) | UI updates, formatting, routing, direct Core Data access | | Presenter | View logic, data formatting (bridges logic & display) | Business logic, network calls, data persistence | | Entity | Plain model objects (PONSOs - no behavior) | Logic, formatting, self-persistence | | Router | Navigation, module assembly | Business logic, data management, UI updates |

VIPER Data Flow

Critical Rule: Entities never pass to Presentation layer.

User Action → View → Presenter → Interactor → Entity/DataStore
           ↓
      Display ← Presenter ← Simple Data ← Interactor

Example: Weather Feature

User taps refresh
→ View calls presenter.didTapRefresh()
→ Presenter calls interactor.refreshWeather()
→ Interactor fetches from DataManager/API
→ Interactor validates, transforms Entity to simple WeatherData
→ Interactor calls presenter.didReceiveWeather(WeatherData)
→ Presenter formats: "72°F, Sunny"
→ Presenter calls view.display(temperature: "72°F")
→ View updates UILabel

Violations to Reject:

  • ✗ Passing NSManagedObject/Entity to Presenter
  • ✗ Presenter accessing Core Data directly
  • ✗ Presenter making network calls
  • ✗ View calling Interactor directly
  • ✗ ViewController instantiating other ViewControllers

Protocol-Based Communication

Rambler's ViperMcFlurry Pattern:

// Module Input (parent → child communication)
protocol WeatherModuleInput: RamblerViperModuleInput {
    func configureWithLocation(_ location: String)
}

// Module Output (child → parent communication)
protocol WeatherModuleOutput: RamblerViperModuleOutput {
    func weatherModuleDidSelectLocation(_ location: String)
}

// Presenter implements both
class WeatherPresenter: WeatherModuleInput {
    weak var view: WeatherViewInput?
    var interactor: WeatherInteractorInput?
    var router: WeatherRouterInput?
    var moduleOutput: WeatherModuleOutput?

    // From module input
    func configureWithLocation(_ location: String) {
        interactor?.fetchWeather(for: location)
    }
}

// View Protocol
protocol WeatherViewInput: AnyObject {
    func displayTemperature(_ text: String)
    func showLoading()
    func showError(_ message: String)
}

protocol WeatherViewOutput: AnyObject {
    func viewDidLoad()
    func didTapRefresh()
}

// Interactor Protocols
protocol WeatherInteractorInput: AnyObject {
    func fetchWeather(for location: String)
}

protocol WeatherInteractorOutput: AnyObject {
    func didFetchWeather(_ data: WeatherData)
    func didFailWithError(_ error: Error)
}

Rambler Ecosystem Tools

Generamba - Code generator for VIPER modules

gem install generamba
generamba setup
generamba gen WeatherModule rviper_controller

Creates consistent module structure with all protocols and classes.

ViperMcFlurry - Framework for module assembly

  • Provides RamblerViperModuleInput and RamblerViperModuleOutput base protocols
  • Enables factory-based and segue-based module creation
  • Handles module configuration through chaining pattern

Typhoon - Dependency injection (used in Rambler's three-layer architecture)

  • Presentation layer: VIPER
  • BusinessLogic layer: Service-Oriented Architecture
  • Core layer: Compound operations

Testing Strategy (TDD-Friendly)

Order: Interactor → Presenter → View

1. Test Interactor First (pure business logic, no UI):

func testFetchWeatherRequestsDataFromCorrectLocation() {
    let mockDataManager = MockWeatherDataManager()
    interactor.dataManager = mockDataManager

    interactor.fetchWeather(for: "San Francisco")

    XCTAssertEqual(mockDataManager.requestedLocation, "San Francisco")
}

2. Then Test Presenter (data formatting):

func testDidFetchWeatherFormatsTemperatureCorrectly() {
    let mockView = MockWeatherView()
    presenter.view = mockView

    presenter.didFetchWeather(WeatherData(temperature: 20))

    XCTAssertEqual(mockView.displayedTemperature, "68°F")
}

3. Finally Test View (UI state):

func testDisplayTemperatureUpdatesLabel() {
    view.displayTemperature("72°F")

    XCTAssertEqual(view.temperatureLabel.text, "72°F")
}

Common Anti-Patterns

| Anti-Pattern | Why It's Wrong | Correct Approach | |--------------|----------------|------------------| | Presenter accesses Core Data | Violates layer separation, untestable | Move to Interactor, use DataManager abstraction | | View calls Interactor directly | Breaks mediation pattern | All communication through Presenter | | Passing NSManagedObject to Presenter | Entity reaches Presentation layer | Transform to simple struct/PONSO in Interactor | | ViewController instantiates other VCs | Tight coupling, hard to test navigation | Router handles all navigation | | Business logic in Presenter | Wrong layer, duplicates testing effort | Move to Interactor |

Example: Refactoring MVC to VIPER

Before (MVC - Massive View Controller):

class WeatherViewController: UIViewController {
    var weatherService = WeatherAPIService()

    func loadWeather() {
        weatherService.fetchWeather { weather in
            self.temperatureLabel.text = "\(weather.temperature)°"
        }
    }
}

After (VIPER - Proper Separation):

// View - Displays only
class WeatherViewController: UIViewController {
    var output: WeatherViewOutput?

    override func viewDidLoad() {
        output?.viewDidLoad()
    }
}

extension WeatherViewController: WeatherViewInput {
    func displayTemperature(_ text: String) {
        temperatureLabel.text = text
    }
}

// Presenter - Formats data
class WeatherPresenter: WeatherViewOutput {
    weak var view: WeatherViewInput?
    var interactor: WeatherInteractorInput?

    func viewDidLoad() {
        interactor?.fetchWeather()
    }

    func didFetchWeather(_ data: WeatherData) {
        let formatted = "\(Int(data.temperature))°F"
        view?.displayTemperature(formatted)
    }
}

// Interactor - Business logic
class WeatherInteractor: WeatherInteractorInput {
    weak var output: WeatherInteractorOutput?
    var dataManager: WeatherDataManager?

    func fetchWeather() {
        dataManager?.fetch { [weak self] result in
            switch result {
            case .success(let weather):
                self?.output?.didFetchWeather(weather)
            case .failure(let error):
                self?.output?.didFailWithError(error)
            }
        }
    }
}

Real-World Impact

Testability: Each layer testable in isolation without mocks for adjacent layers Maintainability: Clear boundaries prevent feature creep in components Team Scalability: Multiple developers can work on different modules without conflicts Code Reuse: Interactor logic works on iOS, iPadOS, macOS identically

References

  • The Book of VIPER: https://github.com/strongself/The-Book-of-VIPER (Rambler team's complete guide)
  • ViperMcFlurry: https://github.com/rambler-digital-solutions/ViperMcFlurry (Rambler's VIPER framework)
  • Generamba: https://github.com/rambler-digital-solutions/Generamba (Code generator)
  • Example App: https://github.com/rambler-digital-solutions/rambler-it-ios (Open source VIPER app)
  • objc.io Article: https://www.objc.io/issues/13-architecture/viper/ (Foundational VIPER explanation)