VIPER: Conceptos clave, el patrón View-Interactor-Presenter-Entity-Router

Autor: IT Sectr Publicado: 2026-02-16 Tiempo de lectura: 9 min

VIPER (View-Interactor-Presenter-Entity-Router) es una arquitectura modular desarrollada en Mutual Mobile para aplicaciones iOS. VIPER divide la aplicación en cinco capas: View se encarga de la visualización, Interactor de la lógica de negocio, Presenter de la preparación de datos, Entity de los modelos de datos, Router de la navegación entre módulos. VIPER es la implementación más detallada del principio de responsabilidad única entre las arquitecturas móviles. Más información en el artículo en objc.io.

Puntos clave

  • VIPER — cinco componentes: View, Interactor, Presenter, Entity, Router con límites de responsabilidad claros
  • Modularidad — cada pantalla (módulo) está aislada, comunicación mediante protocolos
  • Router — saca la navegación de Presenter, resolviendo el problema de navegación en iOS
  • Interactor — contiene la lógica de negocio y no depende de UIKit, se prueba con tests unitarios
  • iOS-native — VIPER fue creado para UIKit antes de SwiftUI y sigue siendo el estándar para grandes proyectos iOS

¿Qué es VIPER? Cinco componentes de la arquitectura modular

VIPER (View-Interactor-Presenter-Entity-Router) es un patrón arquitectónico desarrollado en 2013–2014 en Mutual Mobile para grandes proyectos iOS. Cada pantalla de la aplicación es un módulo separado de cinco componentes con responsabilidades estrictamente definidas. VIPER es la implementación más estricta del Principio de Responsabilidad Única en el desarrollo móvil: ningún componente hace lo que otro puede hacer.

View es un componente pasivo responsable solo de mostrar datos pasados por Presenter. View no contiene lógica de negocio, no maneja navegación, no hace solicitudes de red. En iOS — UIViewController con un ViewProtocol. Interactor es la capa de lógica de negocio que trabaja con Entity y servicios (red, BD, GPS). Interactor no importa UIKit. Presenter es el mediador entre View e Interactor: recibe datos de Interactor, los formatea para mostrarlos y los pasa a View. Presenter tampoco importa UIKit. Entity — modelos de datos (struct, class). Router — gestiona la navegación: crea módulos, abre pantallas, pasa datos entre módulos.

ComponenteResponsabilidadDependencias
ViewVisualización, animaciones, gestosUIKit (solo View)
InteractorLógica de negocio, red, BDEntity, servicios
PresenterFormateo de datos, comandos de ViewViewProtocol, Interactor
EntityModelos de datosNinguna
RouterNavegación, creación de módulosUIViewController (para transiciones)

Las relaciones entre componentes se describen mediante protocolos. ViewProtocol define métodos de visualización, InteractorProtocol define métodos de lógica de negocio, PresenterProtocol define métodos de manejo de eventos, RouterProtocol define métodos de navegación. Cada componente se comunica con otro solo a través de un protocolo, lo que permite reemplazar fácilmente las implementaciones y realizar pruebas aisladas. En promedio, un módulo VIPER para una pantalla contiene 5 protocolos + 5 clases + 1 Builder/Assembler = 11 archivos por pantalla.

VIPER en Swift: Módulo, Router y Presenter

La construcción de un módulo VIPER se realiza en Builder (o Assembler), que crea los cinco componentes y los conecta mediante protocolos. Builder es el único lugar donde los componentes conocen los tipos concretos de los demás. Después del ensamblaje, View se devuelve hacia afuera para su visualización, el resto de la cadena es limpia y se prueba de forma aislada.

swift
// Protocolo — View
protocol UserViewProtocol: AnyObject {
    func display(name: String)
    func display(email: String)
    func showLoading()
    func hideLoading()
}

// Protocolo — Interactor
protocol UserInteractorProtocol {
    func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void)
}

// Protocolo — Router
protocol UserRouterProtocol {
    func navigateToProfile(userId: Int)
}

// Interactor — lógica de negocio
final class UserInteractor: UserInteractorProtocol {
    private let service: UserService

    init(service: UserService) { self.service = service }

    func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
        service.fetchUser(id: id, completion: completion)
    }
}

// Presenter — preparación de datos
final class UserPresenter {
    private weak var view: UserViewProtocol?
    private let interactor: UserInteractorProtocol
    private let router: UserRouterProtocol

    init(interactor: UserInteractorProtocol, router: UserRouterProtocol) {
        self.interactor = interactor
        self.router = router
    }

    func setView(_ view: UserViewProtocol) {
        self.view = view
    }

    func viewDidLoad() {
        view?.showLoading()
        interactor.fetchUser(id: 42) { [weak self] result in
            guard let self else { return }
            self.view?.hideLoading()
            switch result {
            case .success(let user):
                self.view?.display(name: user.name)
                self.view?.display(email: user.email)
            case .failure(let error):
                // manejo de error
            }
        }
    }
}

// Router — navegación
final class UserRouter: UserRouterProtocol {
    private weak var viewController: UIViewController?

    func setViewController(_ vc: UIViewController) {
        viewController = vc
    }

    func navigateToProfile(userId: Int) {
        let profileModule = ProfileModuleBuilder.build(userId: userId)
        viewController?.navigationController?.pushViewController(profileModule, animated: true)
    }
}

// Builder — ensamblaje del módulo
enum UserModuleBuilder {
    static func build(userId: Int) -> UIViewController {
        let service = UserService()
        let interactor = UserInteractor(service: service)
        let router = UserRouter()
        let presenter = UserPresenter(interactor: interactor, router: router)
        let viewController = UserViewController(presenter: presenter)
        presenter.setView(viewController)
        router.setViewController(viewController)
        return viewController
    }
}

Builder/Assembler es un elemento clave de VIPER, que implementa la Inyección de Dependencias manualmente. La inyección de dependencias a través del constructor (constructor injection) garantiza que un componente no pueda crearse sin sus dependencias. En VIPER moderno, Builder puede usar Swinject (contenedor DI), pero el ensamblaje manual sigue siendo más transparente para las pruebas. En IT Sectr, aplicamos VIPER con ensamblaje manual para módulos con lógica compleja — esto simplifica la lectura del código para nuevos desarrolladores.

Mapper (Formatter) es un sexto componente opcional de VIPER. Mapper transforma Entity (modelos BD/servidor) en ViewModel (modelos de visualización). Entity contiene UserDTO con campos id, first_name, last_name, email. ViewModel — UserDisplayItem con name (first_name + last_name) y email. Mapper se ejecuta en Presenter. Si el mapeo de datos es complejo (múltiples Entity → un ViewModel), Mapper se extrae en una clase separada para pruebas.

Comunicación entre módulos VIPER

Los módulos VIPER están aislados y no se conocen entre sí. La comunicación entre módulos ocurre a través de Router. Cuando un usuario hace clic en el botón "Perfil" en la pantalla de usuario, Presenter llama a router.navigateToProfile(userId: 42). Router crea un nuevo módulo mediante ProfileModuleBuilder.build(userId: 42) y lo abre mediante navigationController.push. Flujo de datos: Módulo A → Router A → Builder del Módulo B → El Módulo B se crea y se abre.

Devolución de datos (por ejemplo, seleccionar una ciudad en la pantalla de selección → volver a la pantalla de edición de perfil) en VIPER se implementa mediante delegados o closures. El Módulo B define el protocolo ModuleBDelegate con el método didSelectCity(_ city: City). El Módulo A implementa este protocolo. Router A pasa el delegado al Builder del Módulo B. Cuando se selecciona una ciudad, el Módulo B llama a delegate?.didSelectCity(city). Esta es una práctica estándar de iOS, familiar para cualquier desarrollador de UIKit.

EscenarioMecanismoEjemplo
Navegación hacia adelanteRouter → BuildernavigateToProfile(userId:)
Devolución de datosDelegatedidSelectCity(_:)
Notificación del sistemaNotificationCenterUserDidLogout
Evento desde InteractorPresenter → ViewMensaje WebSocket

NotificationCenter se utiliza para eventos del sistema (cierre de sesión, cambio de plan, notificaciones push) que afectan a varios módulos simultáneamente. Router o AppDelegate se suscribe a Notification, crea el módulo necesario o actualiza el estado. VIPER no prohíbe NotificationCenter — es importante que se use solo para eventos 1-to-many, mientras que para comunicación 1-to-1 se usen delegados o closures.

Comparación de VIPER con MVVM y Clean Architecture

VIPER vs MVVM — VIPER requiere 2–3 veces más código por pantalla pero proporciona un aislamiento absoluto de componentes. MVVM con ViewModel + SwiftUI es más simple y rápido, pero escala peor para equipos de 5+ desarrolladores. VIPER define estrictamente quién es responsable de qué: Interactor — solo lógica de negocio, Presenter — formateo, Router — navegación. En MVVM, ViewModel a menudo crece, tomando la navegación y la lógica de negocio.

VIPER vs Clean Architecture — VIPER es un caso específico de Clean Architecture adaptado para iOS UIKit. Interactor = Use Case, Entity = Domain Model, Presenter = Presentation, Router = Controller en términos de Robert Martin. Clean Architecture añade una capa Gateway/Repository entre Interactor y datos, que generalmente no se separa en VIPER. Para proyectos modernos con SwiftUI, la mayoría de los equipos elige Clean Architecture (The Composable Architecture) o MVVM, dejando VIPER para legados en UIKit.

Cuándo elegir VIPER — equipos de 5+ desarrolladores, proyectos UIKit con 50+ pantallas, requisitos de prueba superiores al 80%, solo iOS (VIPER no es portable a Android sin reescritura). VIPER proporciona una estructura predecible: un nuevo desarrollador entiende un módulo en 15 minutos. Sin embargo, la velocidad de desarrollo es 20–30% menor en comparación con MVVM debido a la mayor cantidad de archivos. En IT Sectr, usamos VIPER para proyectos empresariales UIKit con equipos de 3+ personas y preferimos Clean Architecture para nuevos proyectos en SwiftUI.

Pruebas de módulos VIPER

VIPER está diseñado para pruebas — cada componente se prueba de forma aislada mediante protocolos. Interactor se prueba con servicios mock: se verifica que fetchUser se llame con el ID correcto y que el resultado se pase a Presenter. Presenter se prueba con objetos mock de View e Interactor. Router se prueba con navegación mock: se verifica que navigateToProfile se llame con el userId correcto y que se cree el módulo correcto. View se prueba con tests de UI (XCUITest).

swift
import XCTest

final class UserPresenterTests: XCTestCase {
    func testViewDidLoad_callsFetchUserAndUpdatesView() {
        // Given
        let view = MockUserView()
        let interactor = MockUserInteractor()
        let router = MockUserRouter()
        let presenter = UserPresenter(interactor: interactor, router: router)
        presenter.setView(view)
        let expectedUser = User(id: 42, name: "John", email: "john@test.com")
        interactor.result = .success(expectedUser)

        // When
        presenter.viewDidLoad()

        // Then
        XCTAssertEqual(interactor.capturedUserId, 42)
        XCTAssertEqual(view.displayedName, "John")
        XCTAssertTrue(view.didShowLoading)
    }
}

Objetos mock para VIPER se crean manualmente (clase con propiedades de captura almacenadas) o mediante bibliotecas como Cuckoo / Mockingbird. Las clases mock manuales son más simples y claras, especialmente para la capacitación de nuevos desarrolladores. Cada mock almacena valores capturados (capturedUserId, displayedName) y banderas de llamada (didShowLoading). Al final de la prueba, no solo se verifica que el método fue llamado, sino también con qué parámetros — esto da confianza en la corrección del flujo de datos.

Cobertura de código en proyectos VIPER de IT Sectr alcanza 85–95% para Interactor, 90–95% para Presenter, 70–80% para Router, 30–50% para View (mediante tests de UI). View se prueba con tests de captura de pantalla (SnapshotTesting, 1.5K estrellas) — esto es más rápido que XCUITest y cubre más casos. La cobertura general de un proyecto VIPER suele ser 70–80%, que es más alta que un proyecto MVVM (50–65%), pero requiere más tiempo para escribir pruebas (30–40% del tiempo de desarrollo frente a 20–25% en MVVM).

Preguntas frecuentes

¿Cuántos archivos tiene un módulo VIPER?

Mínimo 11 archivos: 5 protocolos (ViewProtocol, InteractorProtocol, PresenterProtocol, RouterProtocol, Entity), 5 implementaciones (ViewController, Interactor, Presenter, Router, Entity) y Builder/Assembler. Con Mapper (Formatter) — 12–13. Para un proyecto de 50 pantallas, son 550–650 archivos solo de módulos VIPER. MVVM requiere 3 archivos por pantalla (ViewModel, View, Model) — 150 archivos para 50 pantallas.

¿Se puede usar VIPER en Android?

Sí, teóricamente VIPER es portable a Android, pero en la práctica no se usa — Google recomienda MVVM con Jetpack. VIPER fue creado para iOS UIKit, donde ViewController es difícil de probar debido a su ciclo de vida. En Android, Jetpack ViewModel resuelve el problema de prueba sin aislamiento VIPER. El equivalente de VIPER en Android es Clean Architecture con división en módulos/features.

¿Cuál es la diferencia entre VIPER y Clean Architecture?

VIPER es una implementación específica de iOS de Clean Architecture. Interactor corresponde a Use Case, Entity — Domain Model, Presenter — capa de Presentation. Clean Architecture añade Repository/Gateway entre Interactor y datos, que en VIPER generalmente se implementan dentro de Interactor. Clean Architecture no prescribe Router — la navegación queda a discreción de la implementación.

¿Se necesita VIPER para proyectos SwiftUI?

No — SwiftUI está diseñado para MVVM + Combine. VIPER en SwiftUI es redundante: cinco componentes por pantalla con UI declarativa es sobrecarga sin beneficio. Para SwiftUI, elija MVVM o TCA (The Composable Architecture). VIPER sigue siendo relevante para legados UIKit y proyectos donde iOS 12 y versiones anteriores son la versión mínima.

¿Cómo pasar datos entre módulos VIPER?

A través de Router. El Módulo A llama a router.navigateToProfile(userId: id). Router A crea el módulo B mediante Builder, pasa userId. La comunicación de retorno — mediante un delegado: el módulo B define el protocolo ModuleBDelegate, el módulo A lo implementa y lo pasa a través de Router. Eventos del sistema (cierre de sesión) — mediante NotificationCenter.

Resumen

  • VIPER — cinco componentes con separación estricta: View, Interactor, Presenter, Entity, Router
  • Modularidad — cada pantalla está aislada, Builder ensambla dependencias mediante constructor injection
  • Router — saca la navegación de Presenter, resolviendo el problema de navegación en iOS
  • Interactor — lógica de negocio limpia sin UIKit, probada con tests unitarios
  • Volumen de código — 11+ archivos por pantalla, el desarrollo es 20–30% más lento que MVVM
  • Pruebas — 70–80% de cobertura, Interactor y Presenter se prueban mediante objetos mock
  • SwiftUI vs UIKit — VIPER para UIKit (legado), MVVM/TCA para SwiftUI

Desarrollaremos una aplicación móvil llave en mano

IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.

Discutir el proyecto

Lea también