VIPER: kluczowe pojęcia, wzorzec View-Interactor-Presenter-Entity-Router

Autor: IT Sectr Opublikowano: 2026-02-16 Czas czytania: 9 min

VIPER (View-Interactor-Presenter-Entity-Router) — modułowa architektura opracowana w firmie Mutual Mobile dla aplikacji iOS. VIPER dzieli aplikację na pięć warstw: View odpowiada za wyświetlanie, Interactor — za logikę biznesową, Presenter — za przygotowanie danych, Entity — za modele danych, Router — za nawigację między modułami. VIPER to najbardziej szczegółowa implementacja zasady pojedynczej odpowiedzialności wśród architektur mobilnych. Więcej — w artykule na objc.io.

Najważniejsze

  • VIPER — pięć komponentów: View, Interactor, Presenter, Entity, Router z wyraźnymi granicami odpowiedzialności
  • Modułowość — każdy ekran (moduł) jest izolowany, komunikacja przez protokoły
  • Router — przenosi nawigację z Presenter, rozwiązując problem nawigacji w iOS
  • Interactor — zawiera logikę biznesową i nie zależy od UIKit, testowany testami jednostkowymi
  • iOS-native — VIPER został stworzony dla UIKit przed pojawieniem się SwiftUI i pozostaje standardem dla dużych projektów iOS

Czym jest VIPER: pięć komponentów architektury modułowej

VIPER (View-Interactor-Presenter-Entity-Router) — wzorzec architektoniczny opracowany w latach 2013–2014 w firmie Mutual Mobile dla dużych projektów iOS. Każdy ekran aplikacji to oddzielny moduł składający się z pięciu komponentów o ściśle określonych obowiązkach. VIPER to najsurowsza implementacja zasady pojedynczej odpowiedzialności (Single Responsibility Principle) w programowaniu mobilnym: żaden komponent nie robi tego, co może zrobić inny.

View — pasywny komponent odpowiedzialny tylko za wyświetlanie danych przekazanych przez Presenter. View nie zawiera logiki biznesowej, nie obsługuje nawigacji, nie wykonuje zapytań sieciowych. W iOS — UIViewController z protokołem ViewProtocol. Interactor — warstwa logiki biznesowej, współpracująca z Entity i usługami (sieć, baza danych, GPS). Interactor nie importuje UIKit. Presenter — pośrednik między View a Interactor: otrzymuje dane od Interactor, formatuje je do wyświetlenia, przekazuje do View. Presenter również nie importuje UIKit. Entity — modele danych (struct, class). Router — zarządza nawigacją: tworzy moduły, otwiera ekrany, przekazuje dane między modułami.

KomponentOdpowiedzialnośćZależności
ViewWyświetlanie, animacje, gestyUIKit (tylko View)
InteractorLogika biznesowa, sieć, baza danychEntity, usługi
PresenterFormatowanie danych, komendy ViewViewProtocol, Interactor
EntityModele danychBrak
RouterNawigacja, tworzenie modułówUIViewController (do przejść)

Połączenia między komponentami są opisane przez protokoły. ViewProtocol definiuje metody wyświetlania, InteractorProtocol — metody logiki biznesowej, PresenterProtocol — metody obsługi zdarzeń, RouterProtocol — metody nawigacji. Każdy komponent komunikuje się z innym tylko przez protokół, co umożliwia łatwą wymianę implementacji i izolowane testowanie. Przeciętny moduł VIPER z jednego ekranu zawiera 5 protokołów + 5 klas + 1 Builder/Assembler = 11 plików na ekran.

VIPER w Swift: moduł, Router i Presenter

Budowa modułu VIPER jest wykonywana w Builder (lub Assembler), który tworzy wszystkie pięć komponentów i łączy je przez protokoły. Builder to jedyne miejsce, gdzie komponenty znają swoje konkretne typy. Po zbudowaniu View jest zwracane na zewnątrz do wyświetlenia, reszta łańcucha jest czysta i testowana izolowanie.

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

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

// Protokół — Router
protocol UserRouterProtocol {
    func navigateToProfile(userId: Int)
}

// Interactor — logika biznesowa
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 — przygotowanie danych
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):
                // obsługa błędów
            }
        }
    }
}

// Router — nawigacja
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 — budowa modułu
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 — kluczowy element VIPER, implementujący ręczne wstrzykiwanie zależności (Dependency Injection). Wstrzykiwanie zależności przez konstruktor (constructor injection) gwarantuje, że komponent nie może zostać utworzony bez swoich zależności. We współczesnym VIPER Builder może używać Swinject (kontener DI), ale ręczne budowanie pozostaje bardziej przejrzyste dla testowania. W IT Sectr stosujemy VIPER z ręcznym budowaniem dla modułów ze złożoną logiką — upraszcza to czytanie kodu nowym programistom.

Mapper (Formatter) — opcjonalny szósty komponent VIPER. Mapper przekształca Entity (modele bazy danych/serwera) w ViewModel (modele wyświetlania). Entity zawiera UserDTO z polami id, first_name, last_name, email. ViewModel — UserDisplayItem z name (first_name + last_name) i email. Mapper jest wykonywany w Presenter. Jeśli mapowanie danych jest złożone (wiele Entity → jeden ViewModel), Mapper jest wyodrębniany do osobnej klasy w celu testowania.

Komunikacja między modułami VIPER

Moduły VIPER są izolowane i nie wiedzą o sobie nawzajem. Komunikacja między modułami odbywa się przez Router. Gdy użytkownik kliknie przycisk „Profil” na ekranie użytkownika, Presenter wywołuje router.navigateToProfile(userId: 42). Router tworzy nowy moduł przez ProfileModuleBuilder.build(userId: 42) i otwiera go przez navigationController.push. Data flow: Module A → Router A → Module B Builder → Module B zostaje utworzony i otwarty.

Przekazywanie danych z powrotem (np. wybrano miasto na ekranie wyboru → zwrócono na ekran edycji profilu) w VIPER jest realizowane przez delegaty lub domknięcia. Moduł B definiuje protokół ModuleBDelegate z metodą didSelectCity(_ city: City). Moduł A implementuje ten protokół. Router A przekazuje delegata do Module B Builder. Po wyborze miasta Module B wywołuje delegate?.didSelectCity(city). Jest to standardowa praktyka w iOS, znana każdemu programiście UIKit.

ScenariuszMechanizmPrzykład
Przejście do przoduRouter → BuildernavigateToProfile(userId:)
Przekazanie danych wsteczDelegatedidSelectCity(_:)
Powiadomienie systemoweNotificationCenterUserDidLogout
Zdarzenie z InteractorPresenter → ViewWebSocket message

NotificationCenter jest używany do zdarzeń systemowych (wylogowanie, zmiana taryfy, powiadomienia push), które dotyczą kilku modułów jednocześnie. Router lub AppDelegate subskrybuje Notification, tworzy odpowiedni moduł lub aktualizuje stan. VIPER nie zabrania NotificationCenter — ważne, aby był używany tylko dla zdarzeń 1-do-wielu, a dla komunikacji 1-do-1 stosowano delegaty lub domknięcia.

Porównanie VIPER z MVVM i Clean Architecture

VIPER vs MVVM — VIPER wymaga 2–3 razy więcej kodu na ekran, ale zapewnia absolutną izolację komponentów. MVVM z ViewModel + SwiftUI jest prostsze i szybsze, ale gorzej skaluje się w zespołach 5+ programistów. VIPER sztywno określa, kto za co odpowiada: Interactor — tylko logika biznesowa, Presenter — formatowanie, Router — nawigacja. W MVVM ViewModel często rozrasta się, przejmując nawigację i logikę biznesową.

VIPER vs Clean Architecture — VIPER to szczególny przypadek Clean Architecture, zaadaptowany dla iOS UIKit. Interactor = Use Case, Entity = Domain Model, Presenter = Presentation, Router = Controller w terminach Roberta Martina. Clean Architecture dodaje warstwę Gateway/Repository między Interactor a danymi, która w VIPER zwykle nie jest wyodrębniana. Dla nowoczesnych projektów w SwiftUI większość zespołów wybiera Clean Architecture (The Composable Architecture) lub MVVM, pozostawiając VIPER dla starszego kodu na UIKit.

Kiedy wybrać VIPER — zespoły od 5 programistów, projekt na UIKit od 50 ekranów, wymagania dotyczące testowania powyżej 80%, tylko iOS (VIPER nie przenosi się na Android bez przepisywania). VIPER zapewnia przewidywalną strukturę: nowy programista rozumie moduł w 15 minut. Jednak szybkość tworzenia jest o 20–30% niższa w porównaniu z MVVM z powodu większej liczby plików. W IT Sectr używamy VIPER w projektach enterprise na UIKit z zespołami od 3 osób i preferujemy Clean Architecture dla nowych projektów w SwiftUI.

Testowanie modułów VIPER

VIPER jest zaprojektowany do testowania — każdy komponent testowany jest izolowanie przez protokoły. Interactor testuje się z mock-usługami: sprawdza się, czy fetchUser został wywołany z prawidłowym ID i czy wynik został przekazany do Presenter. Presenter testuje się z obiektami mock View i Interactor. Router testuje się z mock-nawigacją: sprawdza się, czy navigateToProfile został wywołany z prawidłowym userId i czy został utworzony prawidłowy moduł. View testuje się testami 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)
    }
}

Obiekty mock dla VIPER tworzone są ręcznie (klasa z przechowywanymi właściwościami-capture) lub przez biblioteki Cuckoo / Mockingbird. Ręczne klasy mock są prostsze i bardziej zrozumiałe, szczególnie do nauki nowych programistów. Każdy mock przechowuje przechwycone wartości (capturedUserId, displayedName) i flagi wywołania (didShowLoading). Na końcu testu sprawdza się nie tylko, czy metoda została wywołana, ale także z jakimi parametrami — to daje pewność poprawności przepływu danych.

Pokrycie kodu w projektach VIPER w IT Sectr osiąga 85–95% dla Interactor, 90–95% dla Presenter, 70–80% dla Router, 30–50% dla View (przez testy UI). View testuje się testami zrzutów ekranu (SnapshotTesting, 1,5K gwiazdek) — jest to szybsze niż XCUITest i pokrywa więcej przypadków. Całkowite pokrycie projektu VIPER wynosi zwykle 70–80%, co jest wyższe niż projektu MVVM (50–65%), ale wymaga więcej czasu na pisanie testów (30–40% czasu tworzenia wobec 20–25% w MVVM).

Często zadawane pytania

Ile plików ma jeden moduł VIPER?

Minimum 11 plików: 5 protokołów (ViewProtocol, InteractorProtocol, PresenterProtocol, RouterProtocol, Entity), 5 implementacji (ViewController, Interactor, Presenter, Router, Entity) i Builder/Assembler. Z Mapper (Formatter) — 12–13. Dla projektu na 50 ekranów to 550–650 plików tylko modułów VIPER. MVVM wymaga 3 pliki na ekran (ViewModel, View, Model) — 150 plików na 50 ekranów.

Czy można używać VIPER na Androidzie?

Tak, teoretycznie VIPER jest przenośny na Android, ale w praktyce nie jest stosowany — Google zaleca MVVM z Jetpack. VIPER został stworzony dla iOS UIKit, gdzie ViewController jest trudny do testowania ze względu na cykl życia. Na Androidzie Jetpack ViewModel rozwiązuje problem testowania bez izolacji VIPER. Androidowym odpowiednikiem VIPER jest Clean Architecture z podziałem na module/feature.

Jaka jest różnica między VIPER a Clean Architecture?

VIPER to specyficzna dla iOS implementacja Clean Architecture. Interactor odpowiada Use Case, Entity — Domain Model, Presenter — warstwie Presentation. Clean Architecture dodaje Repository/Gateway między Interactor a danymi, które w VIPER są zwykle zaimplementowane wewnątrz Interactor. Clean Architecture nie narzuca Router — nawigacja pozostaje do decyzji implementacji.

Czy VIPER jest potrzebny w projektach SwiftUI?

Nie — SwiftUI jest zaprojektowany pod MVVM + Combine. VIPER w SwiftUI jest nadmiarowy: pięć komponentów na jeden ekran przy deklaratywnym UI to narzut bez korzyści. Dla SwiftUI wybierz MVVM lub TCA (The Composable Architecture). VIPER pozostaje aktualny dla starszego kodu UIKit i projektów, gdzie iOS 12 i nowsze to minimalna wersja.

Jak przekazać dane między modułami VIPER?

Przez Router. Moduł A wywołuje router.navigateToProfile(userId: id). Router A tworzy moduł B przez Builder, przekazuje userId. Przekazanie zwrotne — przez delegata: moduł B definiuje protokół ModuleBDelegate, moduł A implementuje go i przekazuje przez Router. Zdarzenia systemowe (wylogowanie) — przez NotificationCenter.

Podsumowanie

  • VIPER — pięć komponentów ze ścisłym podziałem: View, Interactor, Presenter, Entity, Router
  • Modułowość — każdy ekran jest izolowany, Builder składa zależności przez constructor injection
  • Router — przenosi nawigację z Presenter, rozwiązując problem nawigacji w iOS
  • Interactor — czysta logika biznesowa bez UIKit, testowany testami jednostkowymi
  • Ilość kodu — 11+ plików na ekran, tworzenie o 20–30% wolniejsze niż MVVM
  • Testowanie — 70–80% pokrycia, Interactor i Presenter testowane przez obiekty mock
  • SwiftUI vs UIKit — VIPER dla UIKit (starszy kod), MVVM/TCA dla SwiftUI

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również