VIPER (View-Interactor-Presenter-Entity-Router) — عمارة معيارية طورتها شركة Mutual Mobile لتطبيقات iOS. يقسم VIPER التطبيق إلى خمس طبقات: View مسؤولة عن العرض، Interactor عن منطق الأعمال، Presenter عن إعداد البيانات، Entity عن نماذج البيانات، Router عن التنقل بين الوحدات. VIPER هو التنفيذ الأكثر تفصيلاً لمبدأ المسؤولية الفردية بين المعماريات المحمولة. اقرأ المزيد في المقال على objc.io.
الخلاصة
VIPER (View-Interactor-Presenter-Entity-Router) — نمط معماري تم تطويره في 2013–2014 في شركة Mutual Mobile للمشاريع الكبيرة لنظام iOS. كل شاشة تطبيق هي وحدة منفصلة من خمسة مكونات بمسؤوليات محددة بدقة. VIPER هو التنفيذ الأكثر صرامة لمبدأ المسؤولية الفردية في تطوير التطبيقات المحمولة: لا مكون يفعل ما يمكن لمكون آخر فعله.
View مكون سلبي مسؤول فقط عن عرض البيانات المرسلة من Presenter. لا يحتوي View على منطق أعمال، ولا يعالج التنقل، ولا يقوم بطلبات الشبكة. في iOS — UIViewController مع ViewProtocol. Interactor هو طبقة منطق الأعمال التي تعمل مع Entity والخدمات (الشبكة، قاعدة البيانات، GPS). Interactor لا يستورد UIKit. Presenter هو الوسيط بين View وInteractor: يستقبل البيانات من Interactor، ويصيغها للعرض، ويمررها إلى View. Presenter أيضًا لا يستورد UIKit. Entity — نماذج البيانات (struct, class). Router — يدير التنقل: ينشئ الوحدات، يفتح الشاشات، يمرر البيانات بين الوحدات.
| المكون | المسؤولية | التبعيات |
|---|---|---|
| View | العرض، الرسوم المتحركة، الإيماءات | UIKit (View فقط) |
| Interactor | منطق الأعمال، الشبكة، قاعدة البيانات | Entity، الخدمات |
| Presenter | تنسيق البيانات، أوامر View | ViewProtocol، Interactor |
| Entity | نماذج البيانات | لا يوجد |
| Router | التنقل، إنشاء الوحدات | UIViewController (للانتقالات) |
العلاقات بين المكونات توصف بواسطة البروتوكولات. ViewProtocol يحدد طرق العرض، InteractorProtocol يحدد طرق منطق الأعمال، PresenterProtocol يحدد طرق معالجة الأحداث، RouterProtocol يحدد طرق التنقل. كل مكون يتواصل مع آخر فقط عبر بروتوكول، مما يسمح باستبدال التطبيقات بسهولة والاختبار المعزول. في المتوسط، وحدة VIPER لشاشة واحدة تحتوي على 5 بروتوكولات + 5 فئات + 1 Builder/Assembler = 11 ملفًا لكل شاشة.
بناء وحدة VIPER يتم في Builder (أو Assembler)، الذي ينشئ جميع المكونات الخمسة ويصلها عبر البروتوكولات. Builder هو المكان الوحيد الذي تعرف فيه المكونات الأنواع الملموسة لبعضها البعض. بعد التجميع، يتم إرجاع View للخارج للعرض، وبقية السلسلة نظيفة ويتم اختبارها بشكل معزول.
// بروتوكول — 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، ينفذ حقن التبعيات يدويًا. حقن التبعيات عبر المنشئ (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 معزولة ولا تعرف عن بعضها البعض. يحدث التواصل بين الوحدات عبر Router. عندما ينقر المستخدم على زر "الملف الشخصي" في شاشة المستخدم، يستدعي Presenter الدالة router.navigateToProfile(userId: 42). يقوم Router بإنشاء وحدة جديدة عبر ProfileModuleBuilder.build(userId: 42) ويفتحها عبر navigationController.push. تدفق البيانات: الوحدة A → Router A → Builder الوحدة B → يتم إنشاء الوحدة B وفتحها.
إعادة البيانات (مثلاً، اختيار مدينة في شاشة الاختيار → العودة إلى شاشة تحرير الملف الشخصي) في VIPER يتم تنفيذها عبر المفوضين أو الإغلاقات. الوحدة B تحدد بروتوكول ModuleBDelegate بطريقة didSelectCity(_ city: City). الوحدة A تنفذ هذا البروتوكول. Router A يمرر المفوض إلى Builder الوحدة B. عند اختيار مدينة، الوحدة B تستدعي delegate?.didSelectCity(city). هذه ممارسة قياسية في iOS، مألوفة لأي مطور UIKit.
| السيناريو | الآلية | مثال |
|---|---|---|
| التنقل إلى الأمام | Router → Builder | navigateToProfile(userId:) |
| إعادة البيانات | Delegate | didSelectCity(_:) |
| إشعار النظام | NotificationCenter | UserDidLogout |
| حدث من Interactor | Presenter → View | رسالة WebSocket |
NotificationCenter يستخدم لأحداث النظام (تسجيل الخروج، تغيير الخطة، الإشعارات الفورية) التي تؤثر على وحدات متعددة في نفس الوقت. Router أو AppDelegate يشترك في Notification، وينشئ الوحدة المطلوبة أو يحدث الحالة. VIPER لا يمنع NotificationCenter — المهم أنه يستخدم فقط لأحداث 1-to-many، بينما للتواصل 1-to-1 تستخدم المفوضون أو الإغلاقات.
VIPER مقابل MVVM — VIPER يتطلب 2–3 أضعاف الكود لكل شاشة لكنه يوفر عزلًا مطلقًا للمكونات. MVVM مع ViewModel + SwiftUI أبسط وأسرع لكنه أقل قابلية للتوسع مع فرق 5+ مطورين. VIPER يحدد بدقة من المسؤول عن ماذا: Interactor — منطق الأعمال فقط، Presenter — التنسيق، Router — التنقل. في MVVM، ViewModel غالبًا ما ينمو، مستحوذًا على التنقل ومنطق الأعمال.
VIPER مقابل Clean Architecture — VIPER هو حالة خاصة من Clean Architecture مُكيَّفة لـ iOS UIKit. Interactor = Use Case، Entity = Domain Model، Presenter = Presentation، Router = Controller بمصطلحات روبرت مارتن. 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 مصمم للاختبار — كل مكون يختبر بشكل معزول عبر البروتوكولات. Interactor يختبر مع خدمات وهمية: يتم التحقق من أن fetchUser تم استدعاؤه بالمعرف الصحيح وأن النتيجة مرت إلى Presenter. Presenter يختبر مع كائنات وهمية لـ View وInteractor. Router يختبر مع تنقل وهمي: يتم التحقق من أن navigateToProfile تم استدعاؤه بـ userId الصحيح وأن الوحدة الصحيحة تم إنشاؤها. View يختبر باختبارات 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)
}
}
الكائنات الوهمية لـ VIPER تُنشأ يدويًا (فئة بخصائص التقاط مخزنة) أو عبر مكتبات مثل Cuckoo / Mockingbird. الفئات الوهمية اليدوية أبسط وأوضح، خاصة لتدريب المطورين الجدد. كل كائن وهمي يخزن القيم الملتقطة (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).
الأسئلة الشائعة
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، لكن عمليًا لا يُستخدم — Google توصي بـ MVVM مع Jetpack. تم إنشاء VIPER لـ iOS UIKit، حيث ViewController يصعب اختباره بسبب دورة حياته. على Android، Jetpack ViewModel يحل مشكلة الاختبار دون عزل VIPER. المكافئ لـ VIPER على Android هو Clean Architecture مع تقسيم إلى وحدات/ميزات.
VIPER هو تنفيذ خاص بنظام iOS لـ Clean Architecture. Interactor يقابل Use Case، Entity — Domain Model، Presenter — طبقة Presentation. Clean Architecture تضيف Repository/Gateway بين Interactor والبيانات، والتي في VIPER عادة تُنفذ داخل Interactor. Clean Architecture لا تفرض Router — التنقل متروك للتنفيذ.
لا — SwiftUI مصمم لـ MVVM + Combine. VIPER في SwiftUI زائد عن الحاجة: خمسة مكونات لكل شاشة مع UI تصريحي هو عبء دون فائدة. لـ SwiftUI، اختر MVVM أو TCA (The Composable Architecture). VIPER يبقى مناسبًا للأنظمة القديمة UIKit والمشاريع حيث iOS 12 وما دونه هو الإصدار الأدنى.
عبر Router. الوحدة A تستدعي router.navigateToProfile(userId: id). Router A ينشئ الوحدة B عبر Builder، ويمرر userId. التواصل العكسي — عبر مفوض: الوحدة B تحدد بروتوكول ModuleBDelegate، الوحدة A تنفذه وتمرره عبر Router. أحداث النظام (تسجيل الخروج) — عبر NotificationCenter.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا