VIPER (View-Interactor-Presenter-Entity-Router) é uma arquitetura modular desenvolvida na Mutual Mobile para aplicativos iOS. O VIPER divide o aplicativo em cinco camadas: View cuida da exibição, Interactor da lógica de negócios, Presenter da preparação de dados, Entity dos modelos de dados, Router da navegação entre módulos. O VIPER é a implementação mais detalhada do princípio de responsabilidade única entre as arquiteturas móveis. Leia mais no artigo no objc.io.
Principais pontos
VIPER (View-Interactor-Presenter-Entity-Router) é um padrão arquitetural desenvolvido em 2013–2014 na Mutual Mobile para grandes projetos iOS. Cada tela do aplicativo é um módulo separado de cinco componentes com responsabilidades estritamente definidas. O VIPER é a implementação mais rigorosa do Princípio de Responsabilidade Única no desenvolvimento móvel: nenhum componente faz o que outro pode fazer.
View é um componente passivo responsável apenas por exibir dados passados pelo Presenter. View não contém lógica de negócios, não lida com navegação, não faz requisições de rede. No iOS — UIViewController com um ViewProtocol. Interactor é a camada de lógica de negócios que trabalha com Entity e serviços (rede, BD, GPS). Interactor não importa UIKit. Presenter é o mediador entre View e Interactor: recebe dados do Interactor, formata para exibição, passa para View. Presenter também não importa UIKit. Entity — modelos de dados (struct, class). Router — gerencia navegação: cria módulos, abre telas, passa dados entre módulos.
| Componente | Responsabilidade | Dependências |
|---|---|---|
| View | Exibição, animações, gestos | UIKit (apenas View) |
| Interactor | Lógica de negócios, rede, BD | Entity, serviços |
| Presenter | Formatação de dados, comandos View | ViewProtocol, Interactor |
| Entity | Modelos de dados | Nenhuma |
| Router | Navegação, criação de módulos | UIViewController (para transições) |
Relações entre componentes são descritas por protocolos. ViewProtocol define métodos de exibição, InteractorProtocol define métodos de lógica de negócios, PresenterProtocol define métodos de manipulação de eventos, RouterProtocol define métodos de navegação. Cada componente se comunica com outro apenas através de um protocolo, o que permite substituir facilmente implementações e testar isoladamente. Em média, um módulo VIPER para uma tela contém 5 protocolos + 5 classes + 1 Builder/Assembler = 11 arquivos por tela.
A construção de um módulo VIPER é feita no Builder (ou Assembler), que cria todos os cinco componentes e os conecta através de protocolos. O Builder é o único lugar onde os componentes conhecem os tipos concretos uns dos outros. Após a montagem, a View é retornada para exibição, o restante da cadeia é limpo e testado isoladamente.
// 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 negócios
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 — preparação de dados
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):
// tratamento de erro
}
}
}
}
// Router — navegação
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 — montagem do 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 é um elemento chave do VIPER, implementando Injeção de Dependência manualmente. A injeção de dependências através do construtor (constructor injection) garante que um componente não possa ser criado sem suas dependências. No VIPER moderno, o Builder pode usar Swinject (container DI), mas a montagem manual permanece mais transparente para testes. Na IT Sectr, aplicamos VIPER com montagem manual para módulos com lógica complexa — isso simplifica a leitura de código para novos desenvolvedores.
Mapper (Formatter) é um sexto componente opcional do VIPER. O Mapper transforma Entity (modelos BD/servidor) em ViewModel (modelos de exibição). Entity contém UserDTO com campos id, first_name, last_name, email. ViewModel — UserDisplayItem com name (first_name + last_name) e email. O Mapper é executado no Presenter. Se o mapeamento de dados for complexo (várias Entity → um ViewModel), o Mapper é extraído para uma classe separada para testes.
Os módulos VIPER são isolados e não se conhecem. A comunicação entre módulos ocorre através do Router. Quando um usuário clica no botão "Perfil" na tela do usuário, o Presenter chama router.navigateToProfile(userId: 42). O Router cria um novo módulo através de ProfileModuleBuilder.build(userId: 42) e o abre via navigationController.push. Fluxo de dados: Módulo A → Router A → Builder do Módulo B → O Módulo B é criado e aberto.
Devolução de dados (por exemplo, selecionar uma cidade na tela de seleção → retornar à tela de edição de perfil) no VIPER é implementada através de delegados ou closures. O Módulo B define o protocolo ModuleBDelegate com o método didSelectCity(_ city: City). O Módulo A implementa este protocolo. O Router A passa o delegado para o Builder do Módulo B. Quando uma cidade é selecionada, o Módulo B chama delegate?.didSelectCity(city). Esta é uma prática padrão do iOS, familiar para qualquer desenvolvedor UIKit.
| Cenário | Mecanismo | Exemplo |
|---|---|---|
| Navegação para frente | Router → Builder | navigateToProfile(userId:) |
| Devolução de dados | Delegate | didSelectCity(_:) |
| Notificação do sistema | NotificationCenter | UserDidLogout |
| Evento do Interactor | Presenter → View | Mensagem WebSocket |
NotificationCenter é usado para eventos do sistema (logout, mudança de plano, notificações push) que afetam vários módulos simultaneamente. Router ou AppDelegate se inscreve no Notification, cria o módulo necessário ou atualiza o estado. O VIPER não proíbe o NotificationCenter — é importante que ele seja usado apenas para eventos 1-to-many, enquanto para comunicação 1-to-1 são usados delegados ou closures.
VIPER vs MVVM — VIPER requer 2–3 vezes mais código por tela, mas fornece isolamento absoluto de componentes. MVVM com ViewModel + SwiftUI é mais simples e rápido, mas escala pior para equipes de 5+ desenvolvedores. O VIPER define estritamente quem é responsável pelo quê: Interactor — apenas lógica de negócios, Presenter — formatação, Router — navegação. No MVVM, o ViewModel frequentemente cresce, assumindo navegação e lógica de negócios.
VIPER vs Clean Architecture — VIPER é um caso específico do Clean Architecture adaptado para iOS UIKit. Interactor = Use Case, Entity = Domain Model, Presenter = Presentation, Router = Controller nos termos de Robert Martin. O Clean Architecture adiciona uma camada Gateway/Repository entre o Interactor e os dados, que geralmente não é separada no VIPER. Para projetos modernos em SwiftUI, a maioria das equipes escolhe Clean Architecture (The Composable Architecture) ou MVVM, deixando o VIPER para legados em UIKit.
Quando escolher VIPER — equipes de 5+ desenvolvedores, projetos UIKit com 50+ telas, requisitos de teste acima de 80%, apenas iOS (VIPER não é portável para Android sem reescrita). O VIPER fornece uma estrutura previsível: um novo desenvolvedor entende um módulo em 15 minutos. No entanto, a velocidade de desenvolvimento é 20–30% menor em comparação com MVVM devido ao maior número de arquivos. Na IT Sectr, usamos VIPER para projetos empresariais UIKit com equipes de 3+ pessoas e preferimos Clean Architecture para novos projetos em SwiftUI.
VIPER é projetado para testes — cada componente é testado isoladamente através de protocolos. O Interactor é testado com serviços mock: verifica-se que fetchUser é chamado com o ID correto e que o resultado é passado ao Presenter. O Presenter é testado com objetos mock de View e Interactor. O Router é testado com navegação mock: verifica-se que navigateToProfile é chamado com o userId correto e que o módulo correto é criado. A View é testada com testes de UI (XCUITest).
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 são criados manualmente (classe com propriedades de captura armazenadas) ou através de bibliotecas como Cuckoo / Mockingbird. Classes mock manuais são mais simples e claras, especialmente para treinar novos desenvolvedores. Cada mock armazena valores capturados (capturedUserId, displayedName) e flags de chamada (didShowLoading). No final do teste, não só se verifica que o método foi chamado, mas também com quais parâmetros — isso dá confiança na correção do fluxo de dados.
Cobertura de código em projetos VIPER da IT Sectr atinge 85–95% para Interactor, 90–95% para Presenter, 70–80% para Router, 30–50% para View (através de testes de UI). A View é testada com testes de snapshot (SnapshotTesting, 1,5K estrelas) — isso é mais rápido que XCUITest e cobre mais casos. A cobertura geral de um projeto VIPER é geralmente 70–80%, que é maior que um projeto MVVM (50–65%), mas requer mais tempo para escrever testes (30–40% do tempo de desenvolvimento contra 20–25% no MVVM).
Perguntas frequentes
No mínimo 11 arquivos: 5 protocolos (ViewProtocol, InteractorProtocol, PresenterProtocol, RouterProtocol, Entity), 5 implementações (ViewController, Interactor, Presenter, Router, Entity) e Builder/Assembler. Com Mapper (Formatter) — 12–13. Para um projeto de 50 telas, são 550–650 arquivos apenas de módulos VIPER. MVVM requer 3 arquivos por tela (ViewModel, View, Model) — 150 arquivos para 50 telas.
Sim, teoricamente o VIPER é portável para Android, mas na prática não é usado — o Google recomenda MVVM com Jetpack. O VIPER foi criado para iOS UIKit, onde o ViewController é difícil de testar devido ao seu ciclo de vida. No Android, o Jetpack ViewModel resolve o problema de teste sem o isolamento do VIPER. O equivalente do VIPER no Android é o Clean Architecture com divisão em módulos/features.
VIPER é uma implementação específica do iOS do Clean Architecture. Interactor corresponde a Use Case, Entity — Domain Model, Presenter — camada de Presentation. O Clean Architecture adiciona Repository/Gateway entre o Interactor e os dados, que no VIPER geralmente são implementados dentro do Interactor. O Clean Architecture não prescreve Router — a navegação fica a critério da implementação.
Não — SwiftUI é projetado para MVVM + Combine. VIPER no SwiftUI é redundante: cinco componentes por tela com UI declarativa é sobrecarga sem benefício. Para SwiftUI, escolha MVVM ou TCA (The Composable Architecture). O VIPER permanece relevante para legados UIKit e projetos onde iOS 12 e versões anteriores são a versão mínima.
Através do Router. O Módulo A chama router.navigateToProfile(userId: id). O Router A cria o módulo B através do Builder, passa userId. A comunicação de retorno — através de um delegado: o módulo B define o protocolo ModuleBDelegate, o módulo A o implementa e o passa através do Router. Eventos do sistema (logout) — através do NotificationCenter.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também