VIPER: concetti chiave, il pattern View-Interactor-Presenter-Entity-Router

Autore: IT Sectr Pubblicato: 2026-02-16 Tempo di lettura: 9 min

VIPER (View-Interactor-Presenter-Entity-Router) è un'architettura modulare sviluppata in Mutual Mobile per applicazioni iOS. VIPER divide l'applicazione in cinque strati: View gestisce la visualizzazione, Interactor la logica di business, Presenter la preparazione dei dati, Entity i modelli di dati, Router la navigazione tra i moduli. VIPER è l'implementazione più dettagliata del principio di responsabilità singola tra le architetture mobili. Leggi di più nell'articolo su objc.io.

Punti chiave

  • VIPER — cinque componenti: View, Interactor, Presenter, Entity, Router con chiari confini di responsabilità
  • Modularità — ogni schermata (modulo) è isolata, comunicazione tramite protocolli
  • Router — sposta la navigazione fuori da Presenter, risolvendo il problema di navigazione iOS
  • Interactor — contiene la logica di business e non dipende da UIKit, testabile con test unitari
  • iOS-native — VIPER è stato creato per UIKit prima di SwiftUI e rimane lo standard per i grandi progetti iOS

Cos'è VIPER: cinque componenti dell'architettura modulare

VIPER (View-Interactor-Presenter-Entity-Router) è un pattern architetturale sviluppato nel 2013–2014 in Mutual Mobile per grandi progetti iOS. Ogni schermata dell'applicazione è un modulo separato di cinque componenti con responsabilità rigorosamente definite. VIPER è l'implementazione più rigorosa del Principio di Responsabilità Singola (Single Responsibility Principle) nello sviluppo mobile: nessun componente fa ciò che un altro può fare.

View è un componente passivo responsabile solo della visualizzazione dei dati passati da Presenter. View non contiene logica di business, non gestisce la navigazione, non effettua richieste di rete. In iOS — UIViewController con un ViewProtocol. Interactor è il livello di logica di business che lavora con Entity e servizi (rete, DB, GPS). Interactor non importa UIKit. Presenter è il mediatore tra View e Interactor: riceve i dati da Interactor, li formatta per la visualizzazione, li passa a View. Presenter non importa UIKit nemmeno lui. Entity — modelli di dati (struct, class). Router — gestisce la navigazione: crea moduli, apre schermate, passa dati tra moduli.

ComponenteResponsabilitàDipendenze
ViewVisualizzazione, animazioni, gestiUIKit (solo View)
InteractorLogica di business, rete, DBEntity, servizi
PresenterFormattazione dati, comandi ViewViewProtocol, Interactor
EntityModelli di datiNessuna
RouterNavigazione, creazione moduliUIViewController (per transizioni)

Le relazioni tra componenti sono descritte da protocolli. ViewProtocol definisce i metodi di visualizzazione, InteractorProtocol definisce i metodi di logica di business, PresenterProtocol definisce i metodi di gestione eventi, RouterProtocol definisce i metodi di navigazione. Ogni componente comunica con un altro solo tramite un protocollo, consentendo la facile sostituzione delle implementazioni e il test isolato. In media, un modulo VIPER per una schermata contiene 5 protocolli + 5 classi + 1 Builder/Assembler = 11 file per schermata.

VIPER in Swift: Modulo, Router e Presenter

La costruzione di un modulo VIPER viene eseguita in Builder (o Assembler), che crea tutti e cinque i componenti e li collega tramite protocolli. Builder è l'unico luogo in cui i componenti conoscono i tipi concreti degli altri. Dopo l'assemblaggio, View viene restituita all'esterno per la visualizzazione, il resto della catena è pulito e testato isolatamente.

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

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

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

// Interactor — logica di business
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 — preparazione dati
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):
                // gestione errore
            }
        }
    }
}

// Router — navigazione
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 — assemblaggio modulo
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 è un elemento chiave di VIPER, che implementa l'Iniezione di Dipendenze manualmente. L'iniezione di dipendenze tramite costruttore (constructor injection) garantisce che un componente non possa essere creato senza le sue dipendenze. Nel VIPER moderno, Builder può utilizzare Swinject (contenitore DI), ma l'assemblaggio manuale rimane più trasparente per i test. In IT Sectr, applichiamo VIPER con assemblaggio manuale per moduli con logica complessa — questo semplifica la lettura del codice per i nuovi sviluppatori.

Mapper (Formatter) è un sesto componente opzionale di VIPER. Mapper trasforma Entity (modelli DB/server) in ViewModel (modelli di visualizzazione). Entity contiene UserDTO con i campi id, first_name, last_name, email. ViewModel — UserDisplayItem con name (first_name + last_name) ed email. Mapper viene eseguito in Presenter. Se il mapping dei dati è complesso (più Entity → un ViewModel), Mapper viene estratto in una classe separata per il test.

Comunicazione tra moduli VIPER

I moduli VIPER sono isolati e non si conoscono tra loro. La comunicazione tra moduli avviene tramite Router. Quando un utente fa clic sul pulsante "Profilo" nella schermata utente, Presenter chiama router.navigateToProfile(userId: 42). Router crea un nuovo modulo tramite ProfileModuleBuilder.build(userId: 42) e lo apre tramite navigationController.push. Flusso di dati: Modulo A → Router A → Builder Modulo B → Il Modulo B viene creato e aperto.

Restituzione dei dati (ad esempio, selezionare una città nella schermata di selezione → tornare alla schermata di modifica del profilo) in VIPER viene implementata tramite delegati o closure. Il Modulo B definisce il protocollo ModuleBDelegate con il metodo didSelectCity(_ city: City). Il Modulo A implementa questo protocollo. Router A passa il delegato al Builder Modulo B. Quando viene selezionata una città, il Modulo B chiama delegate?.didSelectCity(city). Questa è una pratica standard di iOS, familiare a qualsiasi sviluppatore UIKit.

ScenarioMeccanismoEsempio
Navigazione in avantiRouter → BuildernavigateToProfile(userId:)
Restituzione datiDelegatedidSelectCity(_:)
Notifica di sistemaNotificationCenterUserDidLogout
Evento da InteractorPresenter → ViewMessaggio WebSocket

NotificationCenter viene utilizzato per eventi di sistema (logout, cambio piano, notifiche push) che interessano più moduli contemporaneamente. Router o AppDelegate si iscrive a Notification, crea il modulo necessario o aggiorna lo stato. VIPER non proibisce NotificationCenter — è importante che venga utilizzato solo per eventi 1-a-molti, mentre per la comunicazione 1-a-1 si utilizzano delegati o closure.

Confronto tra VIPER, MVVM e Clean Architecture

VIPER vs MVVM — VIPER richiede 2–3 volte più codice per schermata ma fornisce un isolamento assoluto dei componenti. MVVM con ViewModel + SwiftUI è più semplice e veloce, ma scala peggio per team di 5+ sviluppatori. VIPER definisce rigorosamente chi è responsabile di cosa: Interactor — solo logica di business, Presenter — formattazione, Router — navigazione. In MVVM, ViewModel spesso cresce, assumendo navigazione e logica di business.

VIPER vs Clean Architecture — VIPER è un caso specifico di Clean Architecture adattato per iOS UIKit. Interactor = Use Case, Entity = Domain Model, Presenter = Presentation, Router = Controller nei termini di Robert Martin. Clean Architecture aggiunge un livello Gateway/Repository tra Interactor e dati, che di solito non viene separato in VIPER. Per i progetti moderni in SwiftUI, la maggior parte dei team sceglie Clean Architecture (The Composable Architecture) o MVVM, lasciando VIPER per i legacy UIKit.

Quando scegliere VIPER — team di 5+ sviluppatori, progetti UIKit con 50+ schermate, requisiti di test superiori all'80%, solo iOS (VIPER non è portabile su Android senza riscrittura). VIPER fornisce una struttura prevedibile: un nuovo sviluppatore capisce un modulo in 15 minuti. Tuttavia, la velocità di sviluppo è del 20–30% inferiore rispetto a MVVM a causa del maggior numero di file. In IT Sectr, usiamo VIPER per progetti enterprise UIKit con team di 3+ persone e preferiamo Clean Architecture per i nuovi progetti SwiftUI.

Test dei moduli VIPER

VIPER è progettato per il test — ogni componente viene testato isolatamente tramite protocolli. Interactor viene testato con servizi mock: si verifica che fetchUser venga chiamato con l'ID corretto e che il risultato venga passato a Presenter. Presenter viene testato con oggetti mock di View e Interactor. Router viene testato con navigazione mock: si verifica che navigateToProfile venga chiamato con il userId corretto e che venga creato il modulo corretto. View viene testata con test 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)
    }
}

Oggetti mock per VIPER vengono creati manualmente (classe con proprietà di cattura memorizzate) o tramite librerie come Cuckoo / Mockingbird. Le classi mock manuali sono più semplici e chiare, specialmente per la formazione di nuovi sviluppatori. Ogni mock memorizza valori catturati (capturedUserId, displayedName) e flag di chiamata (didShowLoading). Alla fine del test, non solo si verifica che il metodo sia stato chiamato, ma anche con quali parametri — questo dà fiducia nella correttezza del flusso di dati.

Copertura del codice nei progetti VIPER di IT Sectr raggiunge 85–95% per Interactor, 90–95% per Presenter, 70–80% per Router, 30–50% per View (tramite test UI). View viene testata con test di snapshot (SnapshotTesting, 1,5K stelle) — è più veloce di XCUITest e copre più casi. La copertura complessiva di un progetto VIPER è solitamente del 70–80%, superiore a un progetto MVVM (50–65%), ma richiede più tempo per scrivere i test (30–40% del tempo di sviluppo contro 20–25% in MVVM).

Domande frequenti

Quanti file ci sono in un modulo VIPER?

Almeno 11 file: 5 protocolli (ViewProtocol, InteractorProtocol, PresenterProtocol, RouterProtocol, Entity), 5 implementazioni (ViewController, Interactor, Presenter, Router, Entity) e Builder/Assembler. Con Mapper (Formatter) — 12–13. Per un progetto con 50 schermate, sono 550–650 file solo di moduli VIPER. MVVM richiede 3 file per schermata (ViewModel, View, Model) — 150 file per 50 schermate.

Si può usare VIPER su Android?

Sì, teoricamente VIPER è portabile su Android, ma in pratica non viene utilizzato — Google raccomanda MVVM con Jetpack. VIPER è stato creato per iOS UIKit, dove ViewController è difficile da testare a causa del suo ciclo di vita. Su Android, Jetpack ViewModel risolve il problema del test senza isolamento VIPER. L'equivalente Android di VIPER è Clean Architecture con suddivisione in moduli/feature.

Qual è la differenza tra VIPER e Clean Architecture?

VIPER è un'implementazione specifica per iOS di Clean Architecture. Interactor corrisponde a Use Case, Entity — Domain Model, Presenter — livello Presentation. Clean Architecture aggiunge Repository/Gateway tra Interactor e dati, che in VIPER sono solitamente implementati all'interno di Interactor. Clean Architecture non prescrive Router — la navigazione è lasciata all'implementazione.

Serve VIPER per progetti SwiftUI?

No — SwiftUI è progettato per MVVM + Combine. VIPER in SwiftUI è ridondante: cinque componenti per schermata con UI dichiarativa è un overhead senza vantaggio. Per SwiftUI, scegli MVVM o TCA (The Composable Architecture). VIPER rimane rilevante per legacy UIKit e progetti dove iOS 12 e versioni precedenti sono la versione minima.

Come passare dati tra moduli VIPER?

Tramite Router. Il Modulo A chiama router.navigateToProfile(userId: id). Router A crea il modulo B tramite Builder, passa userId. La comunicazione di ritorno — tramite un delegato: il modulo B definisce il protocollo ModuleBDelegate, il modulo A lo implementa e lo passa tramite Router. Eventi di sistema (logout) — tramite NotificationCenter.

Riepilogo

  • VIPER — cinque componenti con separazione rigorosa: View, Interactor, Presenter, Entity, Router
  • Modularità — ogni schermata è isolata, Builder assembla le dipendenze tramite constructor injection
  • Router — sposta la navigazione fuori da Presenter, risolvendo il problema di navigazione su iOS
  • Interactor — logica di business pulita senza UIKit, testabile con test unitari
  • Volume di codice — 11+ file per schermata, sviluppo 20–30% più lento di MVVM
  • Test — copertura 70–80%, Interactor e Presenter testati tramite oggetti mock
  • SwiftUI vs UIKit — VIPER per UIKit (legacy), MVVM/TCA per SwiftUI

Svilupperemo un'applicazione mobile chiavi in mano

IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.

Discuti il progetto

Leggi anche