VIPER: βασικές έννοιες, το μοτίβο View-Interactor-Presenter-Entity-Router

Συγγραφέας: IT Sectr Δημοσιεύτηκε: 2026-02-16 Χρόνος ανάγνωσης: 9 λεπ

VIPER (View-Interactor-Presenter-Entity-Router) — αρθρωτή αρχιτεκτονική που αναπτύχθηκε στην εταιρεία Mutual Mobile για εφαρμογές iOS. Το VIPER χωρίζει την εφαρμογή σε πέντε επίπεδα: το View είναι υπεύθυνο για την προβολή, το Interactor — για την επιχειρηματική λογική, το Presenter — για την προετοιμασία δεδομένων, το Entity — για τα μοντέλα δεδομένων, το Router — για την πλοήγηση μεταξύ μονάδων. Το VIPER είναι η πιο λεπτομερής υλοποίηση της αρχής της ενιαίας ευθύνης μεταξύ των κινητών αρχιτεκτονικών. Περισσότερα — στο άρθρο στο objc.io.

Κύρια σημεία

  • VIPER — πέντε στοιχεία: View, Interactor, Presenter, Entity, Router με σαφή όρια ευθύνης
  • Αρθρωτότητα — κάθε οθόνη (μονάδα) είναι απομονωμένη, επικοινωνία μέσω πρωτοκόλλων
  • Router — βγάζει την πλοήγηση από το Presenter, λύνοντας το πρόβλημα πλοήγησης iOS
  • Interactor — περιέχει επιχειρηματική λογική και δεν εξαρτάται από το UIKit, ελέγχεται με unit tests
  • iOS-native — το VIPER δημιουργήθηκε για UIKit πριν την εμφάνιση του SwiftUI και παραμένει το πρότυπο για μεγάλα έργα iOS

Τι είναι το VIPER: πέντε στοιχεία αρθρωτής αρχιτεκτονικής

VIPER (View-Interactor-Presenter-Entity-Router) — ένα αρχιτεκτονικό μοτίβο που αναπτύχθηκε το 2013–2014 στην εταιρεία Mutual Mobile για μεγάλα έργα iOS. Κάθε οθόνη της εφαρμογής είναι μια ξεχωριστή μονάδα από πέντε στοιχεία με αυστηρά καθορισμένες ευθύνες. Το VIPER είναι η πιο αυστηρή υλοποίηση της αρχής της ενιαίας ευθύνης (Single Responsibility Principle) στην κινητή ανάπτυξη: κανένα στοιχείο δεν κάνει αυτό που μπορεί να κάνει ένα άλλο.

View — παθητικό στοιχείο υπεύθυνο μόνο για την προβολή δεδομένων που μεταδίδονται από το Presenter. Το View δεν περιέχει επιχειρηματική λογική, δεν διαχειρίζεται πλοήγηση, δεν κάνει αιτήματα δικτύου. Στο iOS — UIViewController με πρωτόκολλο ViewProtocol. Interactor — επίπεδο επιχειρηματικής λογικής που δουλεύει με το Entity και υπηρεσίες (δίκτυο, βάση δεδομένων, GPS). Το Interactor δεν κάνει import το UIKit. Presenter — μεσάζων μεταξύ View και Interactor: λαμβάνει δεδομένα από το Interactor, τα μορφοποιεί για προβολή, τα μεταδίδει στο View. Το Presenter επίσης δεν κάνει import το UIKit. Entity — μοντέλα δεδομένων (struct, class). Router — διαχειρίζεται την πλοήγηση: δημιουργεί μονάδες, ανοίγει οθόνες, μεταδίδει δεδομένα μεταξύ μονάδων.

ΣτοιχείοΕυθύνηΕξαρτήσεις
ViewΠροβολή, κινούμενα σχέδια, χειρονομίεςUIKit (μόνο View)
InteractorΕπιχειρηματική λογική, δίκτυο, βάση δεδομένωνEntity, υπηρεσίες
PresenterΜορφοποίηση δεδομένων, εντολές ViewViewProtocol, Interactor
EntityΜοντέλα δεδομένωνΚανένα
RouterΠλοήγηση, δημιουργία μονάδωνUIViewController (για μεταβάσεις)

Οι συνδέσεις μεταξύ των στοιχείων περιγράφονται από πρωτόκολλα. Το ViewProtocol καθορίζει μεθόδους προβολής, το InteractorProtocol — μεθόδους επιχειρηματικής λογικής, το PresenterProtocol — μεθόδους χειρισμού συμβάντων, το RouterProtocol — μεθόδους πλοήγησης. Κάθε στοιχείο επικοινωνεί με ένα άλλο μόνο μέσω πρωτοκόλλου, το οποίο επιτρέπει την εύκολη αντικατάσταση υλοποιήσεων και την απομονωμένη δοκιμή. Κατά μέσο όρο, μια μονάδα VIPER από μία οθόνη περιέχει 5 πρωτόκολλα + 5 κλάσεις + 1 Builder/Assembler = 11 αρχεία ανά οθόνη.

VIPER σε Swift: μονάδα, Router και Presenter

Η κατασκευή της μονάδας VIPER εκτελείται στο Builder (ή Assembler), το οποίο δημιουργεί και τα πέντε στοιχεία και τα συνδέει μέσω πρωτοκόλλων. Το Builder είναι το μοναδικό μέρος όπου τα στοιχεία γνωρίζουν τους συγκεκριμένους τύπους το ένα του άλλου. Μετά την κατασκευή, το View επιστρέφεται προς τα έξω για προβολή, η υπόλοιπη αλυσίδα είναι καθαρή και δοκιμάζεται απομονωμένα.

swift
// Πρωτόκολλο — View
protocol UserViewProtocol: AnyObject {
    func display(name: String)
    func display(email: String)
    func showLoading()
    func hideLoading()
}

// Πρωτόκολλο — Interactor
protocol UserInteractorProtocol {
    func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void)
}

// Πρωτόκολλο — Router
protocol UserRouterProtocol {
    func navigateToProfile(userId: Int)
}

// Interactor — επιχειρηματική λογική
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 — προετοιμασία δεδομένων
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):
                // χειρισμός σφάλματος
            }
        }
    }
}

// Router — πλοήγηση
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 — κατασκευή μονάδας
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 — το βασικό στοιχείο του VIPER που υλοποιεί τη χειροκίνητη έγχυση εξαρτήσεων (Dependency Injection). Η έγχυση εξαρτήσεων μέσω κατασκευαστή (constructor injection) εγγυάται ότι ένα στοιχείο δεν μπορεί να δημιουργηθεί χωρίς τις εξαρτήσεις του. Στο σύγχρονο VIPER, το Builder μπορεί να χρησιμοποιεί Swinject (κοντέινερ DI), αλλά η χειροκίνητη κατασκευή παραμένει πιο διαφανής για δοκιμές. Στην IT Sectr εφαρμόζουμε VIPER με χειροκίνητη κατασκευή για μονάδες με σύνθετη λογική — αυτό απλοποιεί την ανάγνωση κώδικα από νέους προγραμματιστές.

Mapper (Formatter) — ένα προαιρετικό έκτο στοιχείο του VIPER. Το Mapper μετατρέπει το Entity (μοντέλα βάσης δεδομένων/διακομιστή) σε ViewModel (μοντέλα προβολής). Το Entity περιέχει UserDTO με πεδία id, first_name, last_name, email. ViewModel — UserDisplayItem με name (first_name + last_name) και email. Το Mapper εκτελείται στο Presenter. Εάν η αντιστοίχιση δεδομένων είναι σύνθετη (πολλαπλά Entity → ένα ViewModel), το Mapper μεταφέρεται σε ξεχωριστή κλάση για δοκιμή.

Επικοινωνία μεταξύ μονάδων VIPER

Οι μονάδες VIPER είναι απομονωμένες και δεν γνωρίζουν η μία για την άλλη. Η επικοινωνία μεταξύ μονάδων γίνεται μέσω του Router. Όταν ο χρήστης πατάει το κουμπί „Προφίλ" στην οθόνη χρήστη, το Presenter καλεί router.navigateToProfile(userId: 42). Ο Router δημιουργεί μια νέα μονάδα μέσω ProfileModuleBuilder.build(userId: 42) και την ανοίγει μέσω navigationController.push. Ροή δεδομένων: Μονάδα A → Router A → Μονάδα B Builder → Η Μονάδα B δημιουργείται και ανοίγεται.

Μετάδοση δεδομένων πίσω (για παράδειγμα, επιλέχθηκε μια πόλη στην οθόνη επιλογής → επιστράφηκε στην οθόνη επεξεργασίας προφίλ) στο VIPER υλοποιείται μέσω εκπροσώπων (delegate) ή closures. Η Μονάδα B ορίζει το πρωτόκολλο ModuleBDelegate με τη μέθοδο didSelectCity(_ city: City). Η Μονάδα A υλοποιεί αυτό το πρωτόκολλο. Ο Router A μεταδίδει τον εκπρόσωπο στο Module B Builder. Κατά την επιλογή πόλης, η Μονάδα B καλεί delegate?.didSelectCity(city). Αυτή είναι μια τυπική πρακτική iOS, γνωστή σε κάθε προγραμματιστή UIKit.

ΣενάριοΜηχανισμόςΠαράδειγμα
Μετάβαση προς τα εμπρόςRouter → BuildernavigateToProfile(userId:)
Μετάδοση δεδομένων πίσωDelegatedidSelectCity(_:)
Ειδοποίηση συστήματοςNotificationCenterUserDidLogout
Συμβάν από InteractorPresenter → ViewWebSocket message

NotificationCenter χρησιμοποιείται για συμβάντα συστήματος (αποσύνδεση, αλλαγή τιμολογίου, push ειδοποιήσεις) που επηρεάζουν πολλές μονάδες ταυτόχρονα. Ο Router ή το AppDelegate εγγράφεται στο Notification, δημιουργεί την απαραίτητη μονάδα ή ενημερώνει την κατάσταση. Το VIPER δεν απαγορεύει το NotificationCenter — σημαντικό είναι να χρησιμοποιείται μόνο για συμβάντα 1-προς-πολλούς, και για επικοινωνία 1-προς-1 να εφαρμόζονται εκπρόσωποι ή closures.

Σύγκριση VIPER με MVVM και Clean Architecture

VIPER vs MVVM — το VIPER απαιτεί 2–3 φορές περισσότερο κώδικα ανά οθόνη, αλλά παρέχει απόλυτη απομόνωση στοιχείων. Το MVVM με ViewModel + SwiftUI είναι απλούστερο και ταχύτερο, αλλά κλιμακώνεται χειρότερα σε ομάδες 5+ προγραμματιστών. Το VIPER καθορίζει αυστηρά ποιος είναι υπεύθυνος για τι: Interactor — μόνο επιχειρηματική λογική, Presenter — μορφοποίηση, Router — πλοήγηση. Στο MVVM, το ViewModel συχνά μεγαλώνει, αναλαμβάνοντας την πλοήγηση και την επιχειρηματική λογική.

VIPER vs Clean Architecture — το VIPER είναι μια ειδική περίπτωση της Clean Architecture, προσαρμοσμένη για iOS UIKit. Interactor = Use Case, Entity = Domain Model, Presenter = Presentation, Router = Controller στην ορολογία του Robert Martin. Η Clean Architecture προσθέτει ένα επίπεδο Gateway/Repository μεταξύ Interactor και δεδομένων, το οποίο στο VIPER συνήθως δεν διαχωρίζεται. Για σύγχρονα έργα σε SwiftUI, οι περισσότερες ομάδες επιλέγουν Clean Architecture (The Composable Architecture) ή MVVM, αφήνοντας το VIPER για παλαιό κώδικα σε UIKit.

Πότε να επιλέξετε VIPER — ομάδες από 5 προγραμματιστές, έργο σε UIKit από 50 οθόνες, απαιτήσεις δοκιμών πάνω από 80%, μόνο iOS (το VIPER δεν μεταφέρεται σε Android χωρίς επανεγγραφή). Το VIPER παρέχει προβλέψιμη δομή: ένας νέος προγραμματιστής κατανοεί τη μονάδα σε 15 λεπτά. Ωστόσο, η ταχύτητα ανάπτυξης είναι 20–30% χαμηλότερη σε σύγκριση με το MVVM λόγω του μεγαλύτερου αριθμού αρχείων. Στην IT Sectr χρησιμοποιούμε VIPER για επιχειρηματικά έργα σε UIKit με ομάδες από 3 άτομα και προτιμούμε την Clean Architecture για νέα έργα σε SwiftUI.

Δοκιμή μονάδων VIPER

Το VIPER είναι σχεδιασμένο για δοκιμές — κάθε στοιχείο δοκιμάζεται απομονωμένα μέσω πρωτοκόλλων. Το Interactor δοκιμάζεται με mock υπηρεσίες: ελέγχεται αν το fetchUser κλήθηκε με το σωστό ID και αν το αποτέλεσμα μεταδόθηκε στο Presenter. Το Presenter δοκιμάζεται με mock αντικείμενα View και Interactor. Ο Router δοκιμάζεται με mock πλοήγηση: ελέγχεται αν το navigateToProfile κλήθηκε με το σωστό userId και αν δημιουργήθηκε η σωστή μονάδα. Το View δοκιμάζεται με δοκιμές 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)
    }
}

Mock αντικείμενα για VIPER δημιουργούνται χειροκίνητα (κλάση με αποθηκευμένες ιδιότητες capture) ή μέσω βιβλιοθηκών Cuckoo / Mockingbird. Οι χειροκίνητες κλάσεις mock είναι απλούστερες και πιο κατανοητές, ειδικά για την εκπαίδευση νέων προγραμματιστών. Κάθε mock αποθηκεύει καταγεγραμμένες τιμές (capturedUserId, displayedName) και σημαίες κλήσης (didShowLoading). Στο τέλος της δοκιμής ελέγχεται όχι μόνο αν κλήθηκε η μέθοδος, αλλά και με ποιες παραμέτρους — αυτό δίνει εμπιστοσύνη στην ορθότητα της ροής δεδομένων.

Κάλυψη κώδικα στα έργα VIPER της IT Sectr φτάνει 85–95% για Interactor, 90–95% για Presenter, 70–80% για Router, 30–50% για View (μέσω δοκιμών UI). Το View δοκιμάζεται με δοκιμές στιγμιότυπων οθόνης (SnapshotTesting, 1,5K αστέρια) — είναι ταχύτερο από το XCUITest και καλύπτει περισσότερες περιπτώσεις. Η συνολική κάλυψη ενός έργου VIPER είναι συνήθως 70–80%, η οποία είναι υψηλότερη από ένα έργο MVVM (50–65%), αλλά απαιτεί περισσότερο χρόνο για τη σύνταξη δοκιμών (30–40% του χρόνου ανάπτυξης έναντι 20–25% στο MVVM).

Συχνές Ερωτήσεις

Πόσα αρχεία έχει μια μονάδα VIPER;

Τουλάχιστον 11 αρχεία: 5 πρωτόκολλα (ViewProtocol, InteractorProtocol, PresenterProtocol, RouterProtocol, Entity), 5 υλοποιήσεις (ViewController, Interactor, Presenter, Router, Entity) και Builder/Assembler. Με Mapper (Formatter) — 12–13. Για ένα έργο με 50 οθόνες, αυτό σημαίνει 550–650 αρχεία μόνο για μονάδες VIPER. Το MVVM απαιτεί 3 αρχεία ανά οθόνη (ViewModel, View, Model) — 150 αρχεία για 50 οθόνες.

Μπορεί να χρησιμοποιηθεί το VIPER σε Android;

Ναι, θεωρητικά το VIPER είναι μεταφέρσιμο σε Android, αλλά στην πράξη δεν εφαρμόζεται — η Google συνιστά MVVM με Jetpack. Το VIPER δημιουργήθηκε για iOS UIKit, όπου το ViewController είναι δύσκολο να ελεγχθεί λόγω του κύκλου ζωής. Στο Android, το Jetpack ViewModel λύνει το πρόβλημα δοκιμής χωρίς απομόνωση VIPER. Το αντίστοιχο του VIPER σε Android — Clean Architecture με διαίρεση σε module/feature.

Ποια είναι η διαφορά μεταξύ VIPER και Clean Architecture;

Το VIPER είναι μια υλοποίηση της Clean Architecture ειδική για iOS. Το Interactor αντιστοιχεί στο Use Case, το Entity — στο Domain Model, το Presenter — στο επίπεδο Presentation. Η Clean Architecture προσθέτει ένα επίπεδο Repository/Gateway μεταξύ Interactor και δεδομένων, το οποίο στο VIPER συνήθως υλοποιείται εντός του Interactor. Η Clean Architecture δεν ορίζει Router — η πλοήγηση αφήνεται στην κρίση της υλοποίησης.

Είναι απαραίτητο το VIPER για έργα SwiftUI;

Όχι — το SwiftUI είναι σχεδιασμένο για MVVM + Combine. Το VIPER στο SwiftUI είναι υπερβολικό: πέντε στοιχεία για μία οθόνη με δηλωτικό UI είναι επιβάρυνση χωρίς όφελος. Για SwiftUI επιλέξτε MVVM ή TCA (The Composable Architecture). Το VIPER παραμένει σχετικό για παλαιό κώδικα UIKit και έργα όπου το iOS 12 και κάτω είναι η ελάχιστη έκδοση.

Πώς να μεταδώσετε δεδομένα μεταξύ μονάδων VIPER;

Μέσω Router. Η Μονάδα A καλεί router.navigateToProfile(userId: id). Ο Router A δημιουργεί τη μονάδα B μέσω Builder, μεταδίδει το userId. Η επιστροφή — μέσω εκπροσώπου: η μονάδα B ορίζει το πρωτόκολλο ModuleBDelegate, η μονάδα A το υλοποιεί και το μεταδίδει μέσω Router. Συμβάντα συστήματος (αποσύνδεση) — μέσω NotificationCenter.

Σύνοψη

  • VIPER — πέντε στοιχεία με αυστηρό διαχωρισμό: View, Interactor, Presenter, Entity, Router
  • Αρθρωτότητα — κάθε οθόνη είναι απομονωμένη, ο Builder συναρμολογεί εξαρτήσεις μέσω constructor injection
  • Router — βγάζει την πλοήγηση από το Presenter, λύνοντας το πρόβλημα πλοήγησης στο iOS
  • Interactor — καθαρή επιχειρηματική λογική χωρίς UIKit, ελέγχεται με unit tests
  • Ποσότητα κώδικα — 11+ αρχεία ανά οθόνη, ανάπτυξη 20–30% πιο αργή από MVVM
  • Δοκιμές — 70–80% κάλυψη, Interactor και Presenter ελέγχονται μέσω mock αντικειμένων
  • SwiftUI vs UIKit — VIPER για UIKit (παλαιός κώδικας), MVVM/TCA για SwiftUI

Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση

Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.

Συζήτηση έργου

Διαβάστε επίσης