VIPER (View-Interactor-Presenter-Entity-Router) — Mutual Mobile şirketi tarafından iOS uygulamaları için geliştirilmiş modüler bir mimaridir. VIPER uygulamayı beş katmana ayırır: View görüntülemeden, Interactor iş mantığından, Presenter veri hazırlamadan, Entity veri modellerinden, Router modüller arası navigasyondan sorumludur. VIPER, mobil mimariler arasında tek sorumluluk ilkesinin en detaylı uygulamasıdır. Daha fazlası için objc.io'daki makaleye göz atın.
Önemli Noktalar
VIPER (View-Interactor-Presenter-Entity-Router) — 2013–2014 yıllarında Mutual Mobile'da büyük iOS projeleri için geliştirilmiş bir mimari desendir. Uygulamanın her ekranı, kesin olarak tanımlanmış sorumluluklara sahip beş bileşenden oluşan ayrı bir modüldür. VIPER, mobil geliştirmede Tek Sorumluluk İlkesinin (Single Responsibility Principle) en katı uygulamasıdır: hiçbir bileşen, başka bir bileşenin yapabileceğini yapmaz.
View, yalnızca Presenter tarafından iletilen verileri görüntülemekten sorumlu pasif bir bileşendir. View iş mantığı içermez, navigasyonu yönetmez, ağ istekleri yapmaz. iOS'ta — ViewProtocol ile UIViewController. Interactor, Entity ve hizmetlerle (ağ, DB, GPS) çalışan iş mantığı katmanıdır. Interactor UIKit'i içe aktarmaz. Presenter, View ve Interactor arasındaki arabulucudur: Interactor'dan veri alır, görüntüleme için biçimlendirir, View'a iletir. Presenter da UIKit'i içe aktarmaz. Entity — veri modelleri (struct, class). Router — navigasyonu yönetir: modüller oluşturur, ekranları açar, modüller arasında veri iletir.
| Bileşen | Sorumluluk | Bağımlılıklar |
|---|---|---|
| View | Görüntüleme, animasyonlar, jestler | UIKit (yalnızca View) |
| Interactor | İş mantığı, ağ, DB | Entity, hizmetler |
| Presenter | Veri biçimlendirme, View komutları | ViewProtocol, Interactor |
| Entity | Veri modelleri | Hiçbiri |
| Router | Navigasyon, modül oluşturma | UIViewController (geçişler için) |
Bileşenler arasındaki ilişkiler protokollerle tanımlanır. ViewProtocol görüntüleme yöntemlerini, InteractorProtocol iş mantığı yöntemlerini, PresenterProtocol olay işleme yöntemlerini, RouterProtocol navigasyon yöntemlerini tanımlar. Her bileşen diğeriyle yalnızca bir protokol aracılığıyla iletişim kurar, bu da uygulamaların kolayca değiştirilmesine ve izole test edilmesine olanak tanır. Ortalama olarak, bir ekran için VIPER modülü 5 protokol + 5 sınıf + 1 Builder/Assembler = ekran başına 11 dosya içerir.
VIPER modülü oluşturma Builder (veya Assembler) içinde yapılır; bu, beş bileşenin tümünü oluşturur ve protokoller aracılığıyla bağlar. Builder, bileşenlerin birbirlerinin somut türlerini bildiği tek yerdir. Montajdan sonra View, görüntüleme için dışarıya döndürülür, zincirin geri kalanı temizdir ve izole olarak test edilir.
// Protokol — View
protocol UserViewProtocol: AnyObject {
func display(name: String)
func display(email: String)
func showLoading()
func hideLoading()
}
// Protokol — Interactor
protocol UserInteractorProtocol {
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void)
}
// Protokol — Router
protocol UserRouterProtocol {
func navigateToProfile(userId: Int)
}
// Interactor — iş mantığı
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 — veri hazırlama
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):
// hata işleme
}
}
}
}
// Router — navigasyon
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 — modül montajı
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, Bağımlılık Enjeksiyonunu manuel olarak uygulayan VIPER'ın önemli bir öğesidir. Yapıcı aracılığıyla bağımlılık enjeksiyonu (constructor injection), bir bileşenin bağımlılıkları olmadan oluşturulamamasını garanti eder. Modern VIPER'da Builder, Swinject (DI kapsayıcısı) kullanabilir, ancak manuel montaj test için daha şeffaf kalır. IT Sectr'da, karmaşık mantığa sahip modüller için manuel montajlı VIPER uyguluyoruz — bu, yeni geliştiricilerin kodu okumasını kolaylaştırır.
Mapper (Formatter), VIPER'ın isteğe bağlı altıncı bileşenidir. Mapper, Entity'yi (DB/sunucu modelleri) ViewModel'e (görüntüleme modelleri) dönüştürür. Entity, id, first_name, last_name, email alanlarına sahip UserDTO içerir. ViewModel — name (first_name + last_name) ve email ile UserDisplayItem. Mapper, Presenter içinde yürütülür. Veri eşlemesi karmaşıksa (birden çok Entity → bir ViewModel), Mapper test için ayrı bir sınıfa çıkarılır.
VIPER modülleri izole edilmiştir ve birbirlerini bilmezler. Modüller arası iletişim Router aracılığıyla gerçekleşir. Kullanıcı, kullanıcı ekranındaki "Profil" düğmesine tıkladığında, Presenter router.navigateToProfile(userId: 42) çağrısını yapar. Router, ProfileModuleBuilder.build(userId: 42) aracılığıyla yeni bir modül oluşturur ve navigationController.push ile açar. Veri akışı: Modül A → Router A → Modül B Builder → Modül B oluşturulur ve açılır.
Verileri geri iletme (örneğin, seçim ekranında bir şehir seçme → profil düzenleme ekranına geri dönme) VIPER'da delegeler veya closure'lar aracılığıyla uygulanır. Modül B, didSelectCity(_ city: City) yöntemiyle ModuleBDelegate protokolünü tanımlar. Modül A bu protokolü uygular. Router A, delegcyi Modül B Builder'a iletir. Bir şehir seçildiğinde, Modül B delegate?.didSelectCity(city) çağrısını yapar. Bu, her UIKit geliştiricisinin aşina olduğu standart bir iOS uygulamasıdır.
| Senaryo | Mekanizma | Örnek |
|---|---|---|
| İleri navigasyon | Router → Builder | navigateToProfile(userId:) |
| Veri geri iletme | Delegate | didSelectCity(_:) |
| Sistem bildirimi | NotificationCenter | UserDidLogout |
| Interactor'dan olay | Presenter → View | WebSocket mesajı |
NotificationCenter, aynı anda birden çok modülü etkileyen sistem olayları (oturum kapatma, plan değişikliği, push bildirimleri) için kullanılır. Router veya AppDelegate, Notification'a abone olur, gerekli modülü oluşturur veya durumu günceller. VIPER, NotificationCenter'ı yasaklamaz — önemli olan, yalnızca 1-çok olayları için kullanılması, 1-1 iletişim için ise delegeler veya closure'lar kullanılmasıdır.
VIPER vs MVVM — VIPER, ekran başına 2–3 kat daha fazla kod gerektirir ancak mutlak bileşen izolasyonu sağlar. ViewModel + SwiftUI ile MVVM daha basit ve hızlıdır ancak 5+ geliştiricili ekipler için daha az ölçeklenebilir. VIPER, kimin neden sorumlu olduğunu katı bir şekilde tanımlar: Interactor — yalnızca iş mantığı, Presenter — biçimlendirme, Router — navigasyon. MVVM'de ViewModel genellikle büyür, navigasyonu ve iş mantığını üstlenir.
VIPER vs Clean Architecture — VIPER, iOS UIKit için uyarlanmış Clean Architecture'ın belirli bir durumudur. Interactor = Use Case, Entity = Domain Model, Presenter = Presentation, Router = Controller (Robert Martin'in terimleriyle). Clean Architecture, Interactor ve veri arasına bir Gateway/Repository katmanı ekler; bu, VIPER'da genellikle ayrılmaz. Modern SwiftUI projeleri için çoğu ekip Clean Architecture (The Composable Architecture) veya MVVM'yi seçer ve VIPER'ı UIKit mirasına bırakır.
VIPER ne zaman seçilmeli — 5+ geliştiricili ekipler, 50+ ekranlı UIKit projeleri, %80'in üzerinde test gereksinimleri, yalnızca iOS (VIPER, yeniden yazılmadan Android'e taşınamaz). VIPER, öngörülebilir bir yapı sağlar: yeni bir geliştirici 15 dakikada bir modülü anlar. Ancak, daha fazla dosya nedeniyle geliştirme hızı MVVM'ye kıyasla %20–30 daha düşüktür. IT Sectr'da, 3+ kişilik ekiplerle kurumsal UIKit projeleri için VIPER kullanıyoruz ve yeni SwiftUI projeleri için Clean Architecture'ı tercih ediyoruz.
VIPER test için tasarlanmıştır — her bileşen protokoller aracılığıyla izole olarak test edilir. Interactor, mock hizmetlerle test edilir: fetchUser'ın doğru ID ile çağrıldığı ve sonucun Presenter'a iletildiği kontrol edilir. Presenter, mock View ve Interactor nesneleriyle test edilir. Router, mock navigasyonla test edilir: navigateToProfile'ın doğru userId ile çağrıldığı ve doğru modülün oluşturulduğu kontrol edilir. View, UI testleriyle (XCUITest) test edilir.
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 nesneler VIPER için manuel olarak (depolanmış yakalama özelliklerine sahip sınıf) veya Cuckoo / Mockingbird gibi kütüphaneler aracılığıyla oluşturulur. Manuel mock sınıfları, özellikle yeni geliştiricileri eğitmek için daha basit ve anlaşılırdır. Her mock, yakalanan değerleri (capturedUserId, displayedName) ve çağrı bayraklarını (didShowLoading) depolar. Testin sonunda, yalnızca yöntemin çağrılıp çağrılmadığı değil, aynı zamanda hangi parametrelerle çağrıldığı da kontrol edilir — bu, veri akışının doğruluğuna güven verir.
Kod kapsamı IT Sectr VIPER projelerinde Interactor için %85–95, Presenter için %90–95, Router için %70–80, View için %30–50 (UI testleri aracılığıyla) seviyelerine ulaşır. View, anlık görüntü testleriyle (SnapshotTesting, 1,5K yıldız) test edilir — bu, XCUITest'ten daha hızlıdır ve daha fazla durumu kapsar. Bir VIPER projesinin genel kapsamı genellikle %70–80'dir, bu bir MVVM projesinden (%50–65) daha yüksektir, ancak test yazmak için daha fazla zaman gerektirir (geliştirme süresinin %30–40'ına karşı MVVM'de %20–25).
Sıkça Sorulan Sorular
En az 11 dosya: 5 protokol (ViewProtocol, InteractorProtocol, PresenterProtocol, RouterProtocol, Entity), 5 uygulama (ViewController, Interactor, Presenter, Router, Entity) ve Builder/Assembler. Mapper (Formatter) ile — 12–13. 50 ekranlı bir proje için, yalnızca VIPER modüllerinden 550–650 dosya olur. MVVM, ekran başına 3 dosya gerektirir (ViewModel, View, Model) — 50 ekran için 150 dosya.
Evet, teorik olarak VIPER Android'e taşınabilir, ancak pratikte kullanılmaz — Google, Jetpack ile MVVM'yi önerir. VIPER, ViewController'ın yaşam döngüsü nedeniyle test edilmesi zor olan iOS UIKit için oluşturuldu. Android'de, Jetpack ViewModel, VIPER izolasyonu olmadan test sorununu çözer. Android'de VIPER'ın eşdeğeri, modül/özellik ayrımına sahip Clean Architecture'dır.
VIPER, Clean Architecture'ın iOS'a özgü bir uygulamasıdır. Interactor, Use Case'e, Entity — Domain Model'e, Presenter — Presentation katmanına karşılık gelir. Clean Architecture, Interactor ve veri arasına Repository/Gateway ekler; VIPER'da bunlar genellikle Interactor içinde uygulanır. Clean Architecture, Router'ı belirlemez — navigasyon uygulamaya bırakılır.
Hayır — SwiftUI, MVVM + Combine için tasarlanmıştır. SwiftUI'da VIPER gereksizdir: bildirimsel UI ile ekran başına beş bileşen, faydasız bir yüktür. SwiftUI için MVVM veya TCA (The Composable Architecture) seçin. VIPER, UIKit mirası ve iOS 12 ve altının minimum sürüm olduğu projeler için geçerliliğini korur.
Router aracılığıyla. Modül A, router.navigateToProfile(userId: id) çağrısını yapar. Router A, Builder aracılığıyla modül B'yi oluşturur, userId'yi iletir. Geri bildirim iletişimi — bir delege aracılığıyla: modül B, ModuleBDelegate protokolünü tanımlar, modül A bunu uygular ve Router aracılığıyla iletir. Sistem olayları (oturum kapatma) — NotificationCenter aracılığıyla.
Özet
Anahtar teslim bir mobil uygulama geliştireceğiz
IT Sectr, 2017'den beri girişimler ve işletmeler için iOS ve Android uygulamaları oluşturmaktadır. Size danışmanlık yapacak ve en iyi çözümü önereceğiz.
Ayrıca okuyun