
تعد VIPER و MVVM حاليًا من أشهر الحلول المعمارية المستخدمة في تطوير التطبيقات الكبيرة التي تتطلب المشاركة في تطوير الفرق الكبيرة التي تم اختبارها جيدًا والمدعومة على المدى الطويل والمتطورة باستمرار. سنحاول في هذه المقالة تطبيقها على مشروع اختبار صغير ، وهو عبارة عن قائمة بجهات اتصال المستخدم مع إمكانية إضافة جهة اتصال جديدة. تحتوي هذه المقالة على مزيد من الممارسة أكثر من التحليلات ، وهي موجهة أساسًا لأولئك الذين هم بالفعل من الناحية النظرية على دراية بهذه البنى ويودون الآن فهم كيف يعمل هذا مع أمثلة محددة. ومع ذلك ، يوجد أيضًا وصف أساسي للهندسة المعمارية ومقارنتها.
هذه المقالة هي ترجمة لمقال رافائيل
سوتشي "مقارنة بنيات MVVM و Viper: متى تستخدم واحدة أو أخرى" . لسوء الحظ ، في مرحلة ما من إنشاء المقال ، تم إعداد "منشور" بدلاً من "ترجمة" ، لذلك عليك أن تكتب هنا.
تعد البنية المصممة جيدًا ضرورية لضمان الدعم المستمر لمشروعك. في هذه المقالة ، سننظر إلى معماريات MVVM و VIPER كبديل عن MVC التقليدي.
MVC هو مفهوم معروف لجميع أولئك الذين شاركوا في تطوير البرمجيات لبعض الوقت. يقسم هذا النمط المشروع إلى ثلاثة أجزاء: نموذج يمثل الكيانات ؛ عرض ، وهو واجهة تفاعل المستخدم ؛ ووحدة التحكم ، المسؤولة عن ضمان التفاعل بين العرض والطراز. هذه هي البنية التي تقدمها لنا Apple لاستخدامها في تطبيقاتنا.
ومع ذلك ، ربما تعلم أن المشاريع تأتي مع الكثير من الوظائف المعقدة: دعم طلبات الشبكة ، والتحليل ، والوصول إلى نماذج البيانات ، وتحويل البيانات للإخراج ، ورد الفعل على أحداث الواجهة ، إلخ. نتيجة لذلك ، تحصل على وحدات تحكم ضخمة تحل المهام المذكورة أعلاه ومجموعة من التعليمات البرمجية التي لا يمكن إعادة استخدامها. بمعنى آخر ، يمكن أن يكون MVC كابوسًا للمطور مع دعم المشروع طويل الأجل. ولكن كيف نضمن نمطية عالية وقابلية لإعادة الاستخدام في مشاريع iOS؟
سننظر إلى بديلين مشهورين للغاية في بنية MVC: MVVM و VIPER. كلاهما مشهور جدًا في مجتمع iOS وأثبتا أنهما يمكن أن يكونا بديلاً رائعًا عن MVC. سنتحدث عن هيكلها ، ونكتب تطبيق مثال وننظر في الحالات عندما يكون من الأفضل استخدام بنية أو أخرى.
مثالسنكتب طلبًا مع جدول جهات اتصال المستخدم. يمكنك استخدام الكود من
هذا المستودع . في مجلدات Starter ، يوجد الهيكل العظمي الأساسي للمشروع ، وفي المجلدات النهائية هو تطبيق مكتمل بالكامل.
سيحتوي التطبيق على شاشتين: على أولهما ، سيتم عرض قائمة جهات الاتصال في جدول ، في الخلية سيكون هناك الاسم الأول والأخير لجهة الاتصال ، بالإضافة إلى الصورة الأساسية بدلاً من صورة المستخدم.

الشاشة الثانية هي شاشة لإضافة جهة اتصال جديدة ، مع حقول إدخال الاسم الأول واسم العائلة وأزرار تم و إلغاء.
MVVMكيف يعمل:
يرمز MVVM إلى
Model-View-ViewModel . هذا النهج يختلف عن MVC في منطق توزيع المسؤولية بين الوحدات.
- الموديل : هذه الوحدة لا تختلف عن تلك الموجودة في MVC. إنه مسؤول عن إنشاء نماذج البيانات وقد يحتوي على منطق عمل. يمكنك أيضًا إنشاء فئات مساعدة ، على سبيل المثال ، فئة مدير لإدارة الكائنات في النموذج ومدير الشبكة لمعالجة طلبات الشبكة والتحليل.
- عرض : وهنا كل شيء يبدأ في التغيير. تغطي وحدة العرض في MVVM الواجهة (الفئات الفرعية من ملفات UIView و .xib و. storyboard) ومنطق العرض (الرسوم المتحركة والعرض) ومعالجة أحداث المستخدم (نقرات الأزرار وما إلى ذلك). هذا يعني أن طرق العرض لديك ستبقى دون تغيير ، بينما سيحتوي ViewController على جزء صغير مما كان موجودًا في MVC ، وبالتالي سينخفض بشكل كبير.
- ViewModel : هذا هو الآن المكان الذي سيتم فيه وضع معظم الكود الذي كان لديك سابقًا في ViewController. تطلب طبقة ViewModel البيانات من النموذج (يمكن أن تكون طلبًا لقاعدة بيانات محلية أو طلب شبكة) وتنقلها مرة أخرى إلى طريقة العرض ، بالفعل بالتنسيق الذي سيتم استخدامه وعرضه هناك. ولكن هذه هي آلية ثنائية الاتجاه أو الإجراءات أو البيانات التي يدخلها المستخدم تمر عبر ViewModel وتحديث النموذج. نظرًا لأن ViewModel يتتبع كل ما يتم عرضه ، فمن المفيد استخدام آلية الربط بين الطبقتين.
مقارنةً بـ MVC ، فأنت تنتقل من بنية تبدو كما يلي:

إلى متغير البنية التالي:

حيث يتم استخدام الفئات والفئات الفرعية من UIView و UIViewController لتنفيذ طريقة العرض.
حسنا ، الآن إلى هذه النقطة. دعنا نكتب مثالا على تطبيقنا باستخدام بنية MVVM.
MVVM اتصالات التطبيقنموذجالصف التالي هو نموذج الاتصال بجهة الاتصال:
import CoreData open class Contact: NSManagedObject { @NSManaged var firstName: String? @NSManaged var lastName: String? var fullName: String { get { var name = "" if let firstName = firstName { name += firstName } if let lastName = lastName { name += " \(lastName)" } return name } } }
تحتوي فئة جهة الاتصال على الحقول
firstName و
lastName بالإضافة إلى خاصية
fullName المحسوبة.
عرضVIEW ما يلي: القصة الرئيسية ، مع وجهات النظر وضعت بالفعل على ذلك ؛ ContactsViewController ، والذي يعرض قائمة جهات الاتصال في جدول ؛ و AddContactViewController مع زوج من التسميات وحقول الإدخال لإضافة الاسم واللقب لجهة الاتصال الجديدة. لنبدأ مع
ContactsViewController . سيبدو الرمز الخاص به كما يلي:
import UIKit class ContactsViewController: UIViewController { @IBOutlet var tableView: UITableView! let contactViewModelController = ContactViewModelController() override func viewDidLoad() { super.viewDidLoad() tableView.tableFooterView = UIView() contactViewModelController.retrieveContacts({ [unowned self] in self.tableView.reloadData() }, failure: nil) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { let addContactNavigationController = segue.destination as? UINavigationController let addContactVC = addContactNavigationController?.viewControllers[0] as? AddContactViewController addContactVC?.contactsViewModelController = contactViewModelController addContactVC?.didAddContact = { [unowned self] (contactViewModel, index) in let indexPath = IndexPath(row: index, section: 0) self.tableView.beginUpdates() self.tableView.insertRows(at: [indexPath], with: .left) self.tableView.endUpdates() } } } extension ContactsViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell") as? ContactsTableViewCell guard let contactsCell = cell else { return UITableViewCell() } contactsCell.cellModel = contactViewModelController.viewModel(at: (indexPath as NSIndexPath).row) return contactsCell } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return contactViewModelController.contactsCount } }
حتى مع نظرة سريعة ، من الواضح أن هذه الفئة تنفذ مهام واجهة الجزء الأكبر. كما أن لديها
تنقلًا في طريقة
preparForSegue (: :) - وهذه هي بالضبط اللحظة التي ستتغير في VIPER عند إضافة طبقة جهاز التوجيه.
دعونا نلقي نظرة فاحصة على ملحق الفئة الذي ينفذ بروتوكول UITableViewDataSource. لا تعمل الوظائف مباشرة مع طراز جهة الاتصال الخاصة بمستخدم جهة الاتصال في طبقة النموذج - وبدلاً من ذلك ، يتلقون البيانات (ممثلة في بنية ContactViewModel) في النموذج الذي سيتم عرضه به ، تم تنسيقه بالفعل باستخدام ViewModelController.
يحدث الشيء نفسه في الدائرة ، والتي تبدأ مباشرة بعد إنشاء جهة اتصال. مهمته الوحيدة هي إضافة صف إلى الجدول وتحديث الواجهة.
أنت الآن بحاجة إلى تأسيس علاقة بين فئة فرعية من UITableViewCell و ViewModel. قد يبدو هذا كالفئة الخلوية لجدول
ContactsTableViewCell :
import UIKit class ContactsTableViewCell: UITableViewCell { var cellModel: ContactViewModel? { didSet { bindViewModel() } } func bindViewModel() { textLabel?.text = cellModel?.fullName } }
وكذلك فئة
AddContactViewController :
import UIKit class AddContactViewController: UIViewController { @IBOutlet var firstNameTextField: UITextField! @IBOutlet var lastNameTextField: UITextField! var contactsViewModelController: ContactViewModelController? var didAddContact: ((ContactViewModel, Int) -> Void)? override func viewDidLoad() { super.viewDidLoad() firstNameTextField.becomeFirstResponder() } @IBAction func didClickOnDoneButton(_ sender: UIBarButtonItem) { guard let firstName = firstNameTextField.text, let lastName = lastNameTextField.text else { return } if firstName.isEmpty || lastName.isEmpty { showEmptyNameAlert() return } dismiss(animated: true) { [unowned self] in self.contactsViewModelController?.createContact(firstName: firstName, lastName: lastName, success: self.didAddContact, failure: nil) } } @IBAction func didClickOnCancelButton(_ sender: UIBarButtonItem) { dismiss(animated: true, completion: nil) } fileprivate func showEmptyNameAlert() { showMessage(title: "Error", message: "A contact must have first and last names") } fileprivate func showMessage(title: String, message: String) { let alertView = UIAlertController(title: title, message: message, preferredStyle: .alert) alertView.addAction(UIAlertAction(title: "Ok", style: .destructive, handler: nil)) present(alertView, animated: true, completion: nil) } }
ومرة أخرى ، يجري العمل بشكل أساسي مع واجهة المستخدم هنا. لاحظ أن AddContactViewController يفوض وظيفة إنشاء جهة الاتصال إلى
ViewModelController في
دالة didClickOnDoneButton (:) .
عرض النموذجحان الوقت للحديث عن طبقة ViewModel الجديدة تمامًا بالنسبة لنا. أولاً ، قم بإنشاء
فئة جهة اتصال
ContactViewModel ستوفر طريقة العرض التي نحتاج إلى عرضها ، وسيتم تحديد وظائف <and> مع المعلمات لفرز جهات الاتصال:
public struct ContactViewModel { var fullName: String } public func <(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() < rhs.fullName.lowercased() } public func >(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() > rhs.fullName.lowercased() }
سيبدو رمز
ContactViewModelController كما يلي:
class ContactViewModelController { fileprivate var contactViewModelList: [ContactViewModel] = [] fileprivate var dataManager = ContactLocalDataManager() var contactsCount: Int { return contactViewModelList.count } func retrieveContacts(_ success: (() -> Void)?, failure: (() -> Void)?) { do { let contacts = try dataManager.retrieveContactList() contactViewModelList = contacts.map() { ContactViewModel(fullName: $0.fullName) } success?() } catch { failure?() } } func viewModel(at index: Int) -> ContactViewModel { return contactViewModelList[index] } func createContact(firstName: String, lastName: String, success: ((ContactViewModel, Int) -> Void)?, failure: (() -> Void)?) { do { let contact = try dataManager.createContact(firstName: firstName, lastName: lastName) let contactViewModel = ContactViewModel(fullName: contact.fullName) let insertionIndex = contactViewModelList.insertionIndex(of: contactViewModel) { $0 < $1 } contactViewModelList.insert(contactViewModel, at: insertionIndex) success?(contactViewModel, insertionIndex) } catch { failure?() } } }
ملاحظة: لا يقدم MVVM تعريفًا دقيقًا لكيفية إنشاء ViewModel. عندما أرغب في إنشاء بنية ذات طبقات أكثر ، فإنني أفضل إنشاء ViewModelController يتفاعل مع طبقة النموذج وسيكون مسؤولاً عن إنشاء كائنات ViewModel.
الشيء الرئيسي الذي يسهل تذكره: يجب عدم مشاركة طبقة ViewModel في العمل مع واجهة المستخدم. لتجنب ذلك ، من الأفضل
عدم استيراد UIKit
أبدًا إلى ملف باستخدام ViewModel.
فئة ContactViewModelController تطلب جهات اتصال من التخزين المحلي ولا تحاول التأثير على طبقة النموذج. تقوم بإرجاع البيانات بالتنسيق الذي تتطلبه طريقة العرض ، وإخطار العرض عند إضافة جهة اتصال جديدة وتغيير البيانات.
في الواقع ، سيكون هذا طلبًا للشبكة ، وليس طلبًا لقاعدة البيانات المحلية ، ولكن لا ينبغي بأي حال أن يكون أي منهم جزءًا من ViewModel - ويجب توفير العمل مع الشبكة والعمل مع قاعدة البيانات المحلية باستخدام مديريهم ( المديرين).
هذا كل شيء عن MVVM. ربما يبدو هذا النهج أكثر قابلية للاختبار والدعم والتوزيع من MVC. الآن دعونا نتحدث عن VIPER ونرى كيف يختلف عن MVVM.
VIPERكيف يعمل:
VIPER هو تطبيق الهندسة النظيفة لمشاريع دائرة الرقابة الداخلية. يتكون هيكلها من: العرض ، التفاعل ، مقدم العرض ، الكيان ، وجهاز التوجيه. إنها حقًا بنية موزعة للغاية ووحدات نمطية تتيح لك مشاركة المسؤولية ، وتغطيها اختبارات الوحدة بشكل جيد للغاية وتجعل كودك قابلاً لإعادة الاستخدام.
- عرض : طبقة واجهة تتضمن عادةً ملفات UIKit (بما في ذلك UIViewController). من المفهوم أنه في أنظمة أكثر توزيعًا ، يجب أن تكون الفئات الفرعية من UIViewController مرتبطة بـ View. في VIPER ، تتشابه الأمور تقريبًا كما في MVVM: View هو المسؤول عن عرض ما يقدمه مقدم العرض وعن إرسال المعلومات أو الإجراءات التي يدخلها المستخدم إلى مقدم العرض.
- المتفاعل : يحتوي على منطق العمل اللازم للتطبيق للعمل. Interactor مسؤول عن استرداد البيانات من Model (طلبات الشبكة أو الطلبات المحلية) ولا يرتبط تنفيذها بأي حال بواجهة المستخدم. من المهم أن تتذكر أن مديري الشبكات والمديرين المحليين ليسوا جزءًا من VIPER ، لكنهم يعاملون كتبعيات منفصلة.
- مقدم العرض : مسؤول عن تنسيق البيانات لعرضها في طريقة العرض. في MVVM في مثالنا ، كان ViewModelController مسؤولاً عن هذا. يتلقى مقدم العرض البيانات من Interactor ، ويقوم بإنشاء مثيل لـ ViewModel (فئة منسقة للعرض الصحيح) ويمررها إلى طريقة العرض. كما يستجيب لإدخال المستخدم من البيانات ، ويطلب بيانات إضافية من قاعدة البيانات ، أو العكس ، ويمررها إليها.
- الكيان : يأخذ جزءًا من مسؤولية طبقة النموذج ، والتي تُستخدم في أبنية أخرى. Entity عبارة عن كائن بيانات بسيط ، بدون منطق عمل ، تتم إدارته بواسطة جرار عبر الإنترنت ومديري بيانات مختلفين.
- جهاز التوجيه : كل منطق التنقل التطبيق. قد يبدو أن هذه ليست الطبقة الأكثر أهمية ، ولكن إذا احتجت ، على سبيل المثال ، إلى إعادة استخدام نفس المشاهدات على كل من iPhone وتطبيق iPad ، فإن الشيء الوحيد الذي يمكن أن يتغير هو كيفية ظهور طرق العرض الخاصة بك على الشاشة. يتيح لك هذا عدم لمس أي طبقات أخرى ما عدا جهاز التوجيه ، والذي سيكون مسؤولاً عن ذلك في كل حالة.
مقارنةً بـ MVVM ، لدى VIPER عدة اختلافات رئيسية في توزيع المسؤولية:
- لديه جهاز توجيه ، طبقة منفصلة مسؤولة عن الملاحة
- الكيانات عبارة عن كائنات بيانات بسيطة ، تعيد توزيع مسؤولية الوصول إلى البيانات من النموذج إلى المتفاعل
- يتم تقاسم مسؤوليات ViewModelController بين Interactor والمقدم
والآن دعونا نكرر نفس التطبيق ، ولكن بالفعل على VIPER. ولكن لسهولة الفهم ، سنقوم فقط بإنشاء وحدة تحكم مع جهات الاتصال. يمكنك العثور على رمز وحدة التحكم لإضافة جهة اتصال جديدة في المشروع باستخدام الرابط (مجلد VIPER Contacts Starter في
هذا المستودع ).
ملاحظة : إذا قررت إنشاء مشروعك على VIPER ، فعليك ألا تحاول إنشاء جميع الملفات يدويًا - يمكنك استخدام أحد
منشئي الشفرات ، على سبيل المثال ، مثل
VIPER Gen أو
Generamba (مشروع Rambler) .
اتصالات VIPER التطبيقعرضيتم تمثيل VIEW بعناصر من Main.storyboard وفئة ContactListView. عرض سلبي للغاية ؛ مهامه الوحيدة هي نقل أحداث الواجهة إلى مقدم العرض وتحديث حالته ، بناءً على إخطار مقدم. هذا ما يبدو عليه رمز
ContactListView :
import UIKit class ContactListView: UIViewController { @IBOutlet var tableView: UITableView! var presenter: ContactListPresenterProtocol? var contactList: [ContactViewModel] = [] override func viewDidLoad() { super.viewDidLoad() presenter?.viewDidLoad() tableView.tableFooterView = UIView() } @IBAction func didClickOnAddButton(_ sender: UIBarButtonItem) { presenter?.addNewContact(from: self) } } extension ContactListView: ContactListViewProtocol { func reloadInterface(with contacts: [ContactViewModel]) { contactList = contacts tableView.reloadData() } func didInsertContact(_ contact: ContactViewModel) { let insertionIndex = contactList.insertionIndex(of: contact) { $0 < $1 } contactList.insert(contact, at: insertionIndex) let indexPath = IndexPath(row: insertionIndex, section: 0) tableView.beginUpdates() tableView.insertRows(at: [indexPath], with: .right) tableView.endUpdates() } } extension ContactListView: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell") else { return UITableViewCell() } cell.textLabel?.text = contactList[(indexPath as NSIndexPath).row].fullName return cell } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return contactList.count } }
عرض يرسل أحداث
viewDidLoad و
didClickOnAddButton إلى مقدم العرض. في الحدث الأول ، سيطلب مقدم العرض البيانات من Interactor ، وفي الحالة الثانية ، يطلب مقدم العرض من جهاز التوجيه التبديل إلى وحدة التحكم لإضافة جهة اتصال جديدة.
يتم استدعاء أساليب بروتوكول ContactListViewProtocol من مقدم العرض إما عند طلب قائمة جهات اتصال ، أو عند إضافة جهة اتصال جديدة. في كلتا الحالتين ، تحتوي البيانات الموجودة في طريقة العرض على المعلومات الضرورية للعرض فقط.
في طريقة العرض أيضًا ، توجد طرق تقوم بتطبيق بروتوكول UITableViewDataSource الذي يملأ الجدول بالبيانات المستلمة.
التفاعلالتفاعل في مثالنا بسيط للغاية. كل ما يفعله هو طلب البيانات من خلال مدير قاعدة البيانات المحلي ، ولا يهمه ما يستخدمه هذا المدير أو CoreData أو Realm أو أي حل آخر. سيكون الرمز في ContactListInteractor كما يلي:
class ContactListInteractor: ContactListInteractorInputProtocol { weak var presenter: ContactListInteractorOutputProtocol? var localDatamanager: ContactListLocalDataManagerInputProtocol? func retrieveContacts() { do { if let contactList = try localDatamanager?.retrieveContactList() { presenter?.didRetrieveContacts(contactList) } else { presenter?.didRetrieveContacts([]) } } catch { presenter?.didRetrieveContacts([]) } } }
بعد أن يستقبل Interactor البيانات المطلوبة ، فإنه يخطر مقدم العرض. أيضًا ، كخيار ، يمكن لـ Interactor إرسال خطأ إلى مقدم العرض ، والذي سيتعين عليه بعد ذلك تنسيق الخطأ في طريقة عرض مناسبة للعرض في طريقة العرض.
ملاحظة : كما لاحظت ، تطبق كل طبقة في VIPER بروتوكولًا. نتيجة لذلك ، تعتمد الفئات على التجريد ، وليس على تنفيذ معين ، وبالتالي تلبية مبدأ انعكاس التبعية (أحد مبادئ SOLID).
مقدم العرضأهم عنصر في الهندسة المعمارية. تمر كل الاتصالات بين العرض وبقية الطبقات (Interactor and Router) عبر مقدم العرض.
ContactListPresenter الرمز:
class ContactListPresenter: ContactListPresenterProtocol { weak var view: ContactListViewProtocol? var interactor: ContactListInteractorInputProtocol? var wireFrame: ContactListWireFrameProtocol? func viewDidLoad() { interactor?.retrieveContacts() } func addNewContact(from view: ContactListViewProtocol) { wireFrame?.presentAddContactScreen(from: view) } } extension ContactListPresenter: ContactListInteractorOutputProtocol { func didRetrieveContacts(_ contacts: [Contact]) { view?.reloadInterface(with: contacts.map() { return ContactViewModel(fullName: $0.fullName) }) } } extension ContactListPresenter: AddModuleDelegate { func didAddContact(_ contact: Contact) { let contactViewModel = ContactViewModel(fullName: contact.fullName) view?.didInsertContact(contactViewModel) } func didCancelAddContact() {} }
بعد تحميل العرض ، يقوم بإخطار مقدم العرض ، والذي بدوره يطلب البيانات من خلال Interactor. عندما ينقر المستخدم فوق زر إضافة جهة اتصال جديدة ، يقوم "العرض" بإخطار "مقدم العرض" ، الذي يرسل طلبًا لفتح شاشة إضافة جهة اتصال جديدة في جهاز التوجيه.
يقوم مقدم العرض أيضًا بتنسيق البيانات وإعادته إلى طريقة العرض بعد الاستعلام عن قائمة جهات الاتصال. وهو مسؤول أيضًا عن تنفيذ بروتوكول AddModuleDelegate. هذا يعني أن مقدم العرض سيتلقى إخطارًا عند إضافة جهة اتصال جديدة ، وإعداد بيانات الاتصال للعرض ونقلها إلى العرض.
كما لاحظت ، فإن مقدم العرض لديه كل فرصة ليصبح مرهقًا جدًا. إذا كان هناك مثل هذا الاحتمال ، فيمكن تقسيم مقدم العرض إلى جزأين: مقدم العرض ، الذي يستقبل البيانات فقط ، وينسقها للعرض ويمررها إلى العرض ؛ ومعالج الأحداث الذي سوف يستجيب لإجراءات المستخدم.
ENTITYهذه الطبقة تشبه الطبقة النموذجية في MVVM. في طلبنا ، يتم تمثيله بواسطة فئة الاتصال ووظائف تعريف المشغل <و>. سيبدو محتوى
جهة الاتصال كما يلي:
import CoreData open class Contact: NSManagedObject { var fullName: String { get { var name = "" if let firstName = firstName { name += firstName } if let lastName = lastName { name += " " + lastName } return name } } } public struct ContactViewModel { var fullName = "" } public func <(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() < rhs.fullName.lowercased() } public func >(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() > rhs.fullName.lowercased() }
يحتوي ContactViewModel على الحقول التي يملأها مقدم العرض (التنسيقات) التي يعرضها العرض. فئة جهة الاتصال هي فئة فرعية من NSManagedObject تحتوي على نفس الحقول كما في نموذج CoreData.
راوتروأخيرا ، الطبقة الأخيرة ، ولكن بالتأكيد ليست في الأهمية. تقع كل مسؤولية الملاحة على عاتق مقدم العرض و WireFrame. يتلقى مقدم العرض حدثًا من المستخدم ويعرف وقت إجراء النقل ، ويعرف WireFrame كيفية ومكان إجراء هذا الانتقال. بحيث لا تشعر بالارتباك ، في هذا المثال ، يتم تمثيل طبقة الموجه بواسطة فئة ContactListWireFrame ويشار إليها باسم WireFrame في النص.
ContactListWireFrame رمز:
import UIKit class ContactListWireFrame: ContactListWireFrameProtocol { class func createContactListModule() -> UIViewController { let navController = mainStoryboard.instantiateViewController(withIdentifier: "ContactsNavigationController") if let view = navController.childViewControllers.first as? ContactListView { let presenter: ContactListPresenterProtocol & ContactListInteractorOutputProtocol = ContactListPresenter() let interactor: ContactListInteractorInputProtocol = ContactListInteractor() let localDataManager: ContactListLocalDataManagerInputProtocol = ContactListLocalDataManager() let wireFrame: ContactListWireFrameProtocol = ContactListWireFrame() view.presenter = presenter presenter.view = view presenter.wireFrame = wireFrame presenter.interactor = interactor interactor.presenter = presenter interactor.localDatamanager = localDataManager return navController } return UIViewController() } static var mainStoryboard: UIStoryboard { return UIStoryboard(name: "Main", bundle: Bundle.main) } func presentAddContactScreen(from view: ContactListViewProtocol) { guard let delegate = view.presenter as? AddModuleDelegate else { return } let addContactsView = AddContactWireFrame.createAddContactModule(with: delegate) if let sourceView = view as? UIViewController { sourceView.present(addContactsView, animated: true, completion: nil) } } }
نظرًا لأن WireFrame مسؤول عن إنشاء الوحدة النمطية ، فسيكون من المناسب تكوين كل التبعيات هنا. عندما تريد فتح وحدة تحكم أخرى ، فإن الوظيفة التي تفتح وحدة التحكم الجديدة تتلقى كحجة الكائن الذي سيفتحه ، وتقوم بإنشاء وحدة تحكم جديدة باستخدام WireFrame. أيضًا ، عند إنشاء وحدة تحكم جديدة ، يتم نقل البيانات اللازمة إليها ، في هذه الحالة فقط المفوض (مقدم وحدة التحكم مع جهات الاتصال) لتلقي جهة الاتصال التي تم إنشاؤها.
توفر طبقة الموجه فرصة جيدة لتجنب استخدام المقاطع (التحولات) في القصص المصورة وتنظيم جميع التنقلات البرمجية. نظرًا لأن لوحات العمل لا توفر حلاً مضغوطًا لنقل البيانات بين وحدات التحكم ، فإن تطبيق التنقل لدينا لن يضيف رمزًا إضافيًا. كل ما نحصل عليه هو فقط أفضل إعادة الاستخدام.
ملخص :
يمكنك العثور على كلا المشروعين في
هذا المستودع .
كما ترون ، MVVM و VIPER ، على الرغم من اختلافهما ، ليست فريدة من نوعها. يخبرنا MVVM أنه إلى جانب العرض والطراز ، يجب أن تكون هناك أيضًا طبقة ViewModel. ولكن لا يوجد شيء يقال حول كيفية إنشاء هذه الطبقة ، ولا حول كيفية طلب البيانات - لم يتم تحديد مسؤولية هذه الطبقة بوضوح. هناك العديد من الطرق لتنفيذه ويمكنك استخدام أي منها.
VIPER ، من ناحية أخرى ، هي بنية فريدة إلى حد ما. وهو يتألف من العديد من الطبقات ، ولكل منها منطقة محددة جيدًا من المسؤولية ويتأثر المطور بأقل من MVVM.
عندما يتعلق الأمر باختيار بنية ، لا يوجد عادة الحل الصحيح الوحيد ، ولكن ما زلت أحاول تقديم بعض النصائح. إذا كان لديك مشروع كبير وطويل ، مع متطلبات واضحة وتريد أن تتاح لك فرصة كافية لإعادة استخدام المكونات ، فسيكون VIPER هو الحل الأفضل. تحديد المسؤولية بشكل أوضح يجعل من الممكن تنظيم الاختبار بشكل أفضل وتحسين إعادة الاستخدام.