كيف توصلنا في QIWI إلى أسلوب شائع للتفاعل بين View و ViewModel ضمن MVVM

في البداية ، تمت كتابة المشروع بالكامل في Objective-C واستخدم ReactiveCocoa الإصدار 2.0


تم تنفيذ التفاعل بين View و ViewModel عن طريق ربط خصائص نموذج العرض ، وسيكون كل شيء على ما يرام ، إلا أن تصحيح مثل هذا الرمز كان صعبًا للغاية. كل ذلك بسبب عدم وجود الكتابة والعصيدة في تتبع المكدس :(


والآن حان الوقت لاستخدام سويفت. في البداية ، قررنا أن نحاول دون تفاعل على الإطلاق. عرض الأساليب التي تم استدعاؤها بشكل صريح على ViewModel ، وأبلغ ViewModel عن تغييراته باستخدام مفوض:


protocol ViewModelDelegate { func didUpdateTitle(newTitle: String) } class View: UIView, ViewModelDelegate { var viewModel: ViewModel func didUpdateTitle(newTitle: String) { //handle viewModel updates } } class ViewModel { weak var delegate: ViewModelDelegate? func handleTouch() { //respond to some user action } } 

انها تبدو جيدة. ولكن مع نمو ViewModel ، بدأنا في الحصول على مجموعة من الأساليب في المفوض للتعامل مع كل العطس الذي ينتج عن ViewModel:


 protocol ViewModelDelegate { func didUpdate(title: String) func didUpdate(subtitle: String) func didReceive(items: [SomeItem]) func didReceive(error: Error) func didChangeLoading(isLoafing: Bool) //...  } 

يجب تنفيذ كل طريقة ، ونتيجة لذلك نحصل على مجموعة كبيرة من الأساليب الموجودة في العرض. لا يبدو رائعا جدا. ليس على الاطلاق بارد. إذا فكرت في ذلك ، إذا كنت تستخدم RxSwift ، فستحصل على موقف مشابه ، ولكن بدلاً من تطبيق أساليب التفويض ، سيكون هناك عدد كبير من المجلدات لخصائص ViewModel المختلفة.


يقترح الإخراج نفسه: تحتاج إلى الجمع بين جميع الأساليب في واحدة وخصائص التعداد شيء مثل هذا:


 enum ViewModelEvent { case updateTitle(String) case updateSubtitle(String) case items([SomeItem]) case error(Error) case loading(Bool) //...  } 

للوهلة الأولى ، لا يتغير الجوهر. ولكن بدلاً من ستة طرق ، نحصل على واحدة مع رمز التبديل:


 func handle(event: ViewModelEvent) { switch event { case .updateTitle(let newTitle): //... case .updateSubtitle(let newSubtitle): //... case .items(let newItems): //... case .error(let error): //... case .loading(let isLoading): //... } } 

للتماثل ، يمكنك إنشاء تعداد آخر ومعالجها في ViewModel:


 enum ViewEvent { case touchButton case swipeLeft } class ViewModel { func handle(event: ViewEvent) { switch event { case .touchButton: //... case .swipeLeft: //... } } } 

يبدو كل شيء أكثر إيجازًا ، بالإضافة إلى أنه يعطي نقطة تفاعل واحدة بين View و ViewModel ، مما يؤثر جيدًا على قابلية قراءة التعليمات البرمجية. اتضح أنه يربح الجميع - ويتم تسريع مراجعة طلب السحب ، وسرعان ما يدخل القادمون الجدد إلى المشروع.


ولكن ليس حلا سحريا. تبدأ المشاكل في الظهور عندما يريد نموذج عرض ما الإبلاغ عن الأحداث الخاصة به إلى عدة طرق عرض ، على سبيل المثال ، ContainerView و ContentView (أحدهما مضمّن في الآخر). الحل ، مرة أخرى ، ينشأ بحد ذاته ، نكتب فئة جديدة بدلاً من المفوض:


 class Output<Event> { var handlers = [(Event) -> Void]() func send(_ event: Event) { for handler in handlers { handler(event) } } } 

في خاصية handlers ، handlers بتخزين الإشارات المرجعية مع استدعاءات handle(event:) طرق handle(event:) ، وعندما نسمي أسلوب send(_ event:) ، فإننا ندعو جميع معالجات هذا الحدث. ومرة أخرى ، يبدو أن المشكلة قد تم حلها ، ولكن في كل مرة تقوم فيها بربط View - ViewModel ، يجب عليك كتابة هذا:


 vm.output.handlers.append({ [weak view] event in DispatchQueue.main.async { view?.handle(event: event) } }) view.output.handlers.append({ [weak vm] event in vm?.handle(event: event) }) 

ليس رائع جدا
نغلق طريقة العرض و ViewModel مع البروتوكولات:


 protocol ViewModel { associatedtype ViewEvent associatedtype ViewModelEvent var output: Output<ViewModelEvent> { get } func handle(event: ViewEvent) func start() } protocol View: ViewModelContainer { associatedtype ViewModelEvent associatedtype ViewEvent var output: Output<ViewEvent> { get } func setupBindings() func handle(event: ViewModelEvent) } 

لماذا هناك حاجة إلى أساليب start() و setupBindings() لاحقًا. نكتب ملحقات للبروتوكول:


 extension View where Self: NSObject { func bind<ViewModelType: ViewModel>(with vm: ViewModelType?) where ViewModelType.ViewModelEvent == ViewModelEvent, ViewModelType.ViewEvent == ViewEvent { guard let vm = vm else { return } vm.output.handlers.append({ [weak self] event in DispatchQueue.main.async { self?.handle(event: event) } }) output.handlers.append({ [weak vm] event in vm?.handle(event: event) }) setupBindings() vm.start() } } 

ونحصل على طريقة جاهزة لربط أي طريقة عرض - ViewModel ، تتطابق أحداثها. تضمن طريقة start() أنه عند تنفيذه ، ستتلقى المشاهدة بالفعل جميع الأحداث التي سيتم إرسالها من setupBindings() وستكون هناك حاجة إلى طريقة setupBindings() إذا كنت بحاجة إلى رمي ViewModel في عروضك الفرعية الخاصة ، لذلك يمكن تنفيذ هذه الطريقة افتراضيًا في ملحق " ه.


اتضح أنه بالنسبة للعلاقة بين View و ViewModel ، فإن تطبيقاتها المحددة ليست مهمة على الإطلاق ، والشيء الرئيسي هو أن يكون View قادرًا على التعامل مع أحداث ViewModel ، والعكس بالعكس. ولتخزين في طريقة العرض لا رابط محدد لـ ViewModel ، ولكن في نسخته المعممة ، يمكنك كتابة مجمّع إضافي من نوع TypeErasure (نظرًا لأنه من المستحيل استخدام خصائص نوع البروتوكول مع النوع associatedtype ):


 class AnyViewModel<ViewModelEvent, ViewEvent>: ViewModel { var output: Output<ViewModelEvent> let startClosure: EmptyClosure let handleClosure: (ViewEvent) -> Void let vm: Any? private var isStarted = false init?<ViewModelType: ViewModel>(with vm: ViewModelType?) where ViewModelType.ViewModelEvent == ViewModelEvent, ViewModelType.ViewEvent == ViewEvent { guard let vm = vm else { return nil } self.output = vm.output self.vm = vm self.startClosure = { [weak vm] in vm?.start() } self.handleClosure = { [weak vm] in vm?.handle(event: $0) }//vm.handle } func start() { if !isStarted { isStarted = true startClosure() } } func handle(event: ViewEvent) { handleClosure(event) } } 

مزيد من أكثر


قررنا الذهاب إلى أبعد من ذلك ، ومن الواضح أننا لا نخزن العقار في العرض ، لكننا حددناه خلال وقت التشغيل ، في الإجمال ، تحول امتداد بروتوكول View كما يلي:


 extension View where Self: NSObject { func bind<ViewModelType: ViewModel>(with vm: ViewModelType?) where ViewModelType.ViewModelEvent == ViewModelEvent, ViewModelType.ViewEvent == ViewEvent { guard let vm = AnyViewModel(with: vm) else { return } vm.output.handlers.append({ [weak self] event in if #available(iOS 10.0, *) { RunLoop.main.perform(inModes: [.default], block: { self?.handle(event: event) }) } else { DispatchQueue.main.async { self?.handle(event: event) } } }) output.handlers.append({ [weak vm] event in vm?.handle(event: event) }) p_viewModelSaving = vm setupBindings() vm.start() } private var p_viewModelSaving: Any? { get { return objc_getAssociatedObject(self, &ViewModelSavingHandle) } set { objc_setAssociatedObject(self, &ViewModelSavingHandle, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } var viewModel: AnyViewModel<ViewModelEvent, ViewEvent>? { return p_viewModelSaving as? AnyViewModel<ViewModelEvent, ViewEvent> } } 

إنها لحظة مثيرة للجدل ، لكننا قررنا أنه سيكون من الملائم أكثر عدم الإعلان عن هذه الخاصية في كل مرة.


قوالب


يتناسب هذا النهج تمامًا مع قوالب Xcode ويسمح لك بإنشاء وحدات بسرعة كبيرة في بضع نقرات. قالب مثال للعرض:


 final class ___VARIABLE_moduleName___ViewController: UIView, View { var output = Output<___VARIABLE_moduleName___ViewModel.ViewEvent>() override func viewDidLoad() { super.viewDidLoad() setupViews() } private func setupViews() { //Do layout and more } func handle(event: ___VARIABLE_moduleName___ViewModel.ViewModelEvent) { } } 

وبالنسبة لـ ViewModel:


 final class ___VARIABLE_moduleName___ViewModel: ViewModel { var output = Output<ViewModelEvent>() func start() { } func handle(event: ViewEvent) { } } extension ___VARIABLE_moduleName___ViewModel { enum ViewEvent { } enum ViewModelEvent { } } 

ولا يستغرق إنشاء تهيئة الوحدة النمطية في التعليمات البرمجية سوى ثلاثة أسطر:


 let viewModel = SomeViewModel() let view = SomeView() view.bind(with: viewModel) 

استنتاج


نتيجة لذلك ، حصلنا على طريقة مرنة لتبادل الرسائل بين View و ViewModel ، التي لها نقطة دخول واحدة وتستند بشكل جيد إلى إنشاء كود Xcode. مكّن هذا النهج من تسريع عملية تطوير الميزات ومراجعات طلب السحب ، إلى جانب زيادة إمكانية قراءة البساطة وبساطة الرمز وتبسيط كتابة الاختبارات (نظرًا لحقيقة أنه من خلال معرفة التسلسل المرغوب فيه لاستقبال الأحداث من نموذج العرض ، فمن السهل كتابة اختبارات الوحدة التي يمكن بها هذا التسلسل يمكن ضمانه). على الرغم من أن هذا النهج قد بدأ استخدامه معنا مؤخرًا ، إلا أننا نأمل أن يبرر ذلك تمامًا ويبسط عملية التطوير إلى حد كبير.


PS


وإعلان صغير لعشاق التطوير لنظام iOS - بالفعل هذا الخميس ، 25 يوليو ، سوف نعقد iOS mitap في ART-SPACE ، الدخول مجاني ، تعال.

Source: https://habr.com/ru/post/ar460875/


All Articles