تجربة استخدام "المنسقين" في مشروع "iOS" حقيقي

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

المشكلة


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

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

القصة


مارتن فاولر ، في كتابه " أنماط هندسة تطبيقات المؤسسات" ، أطلق عليه اسم " مراقب التطبيقات" . وأول مروج له في بيئة "iOS" هو Sorush Khanlu : بدأ كل شيء بتقريره عن "NSSpain" في عام 2015. ثم ظهر مقال مراجعة على موقعه على شبكة الإنترنت ، والذي كان له عدة عواقب (على سبيل المثال ، هذا ).

ثم تمت متابعة الكثير من المراجعات (يقدم استعلام "منسقي ios" العشرات من النتائج من حيث الجودة ودرجة التفصيل المختلفة) ، بما في ذلك حتى دليل على Ray Wenderlich ومقال من بول هدسون عن كتابه "Hacking with Swift" كجزء من سلسلة من المواد حول كيفية التخلص من المشكلة تحكم "هائل".

إذا نظرنا إلى الأمام ، فإن موضوع النقاش الأكثر بروزًا هو مشكلة زر الرجوع في UINavigationController ، والتي لا تتم معالجة النقر عليها من خلال التعليمات البرمجية الخاصة بنا ، ولكن يمكننا فقط الحصول على رد اتصال .

في الواقع ، لماذا هذه مشكلة؟ يحتاج المنسقون ، مثلهم في ذلك مثل أي كائنات ، من أجل الوجود في الذاكرة ، إلى كائن آخر "لامتلاكهم". كقاعدة عامة ، عند إنشاء نظام ملاحة باستخدام المنسقين ، يقوم بعض المنسقين بإنشاء آخرين والحفاظ على ارتباط قوي بهم. عند "مغادرة منطقة مسؤولية" المنسق الأصلي ، تعود السيطرة إلى المنسق الأصلي ، ويجب تحرير الذاكرة التي يشغلها المنشئ.

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

النهج الأول


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

عندما بدأنا في التجربة مع المنسقين في الفريق لأول مرة ، لم يكن لدينا الكثير من الوقت وحرية العمل من أجل هذا: كان من الضروري التفكير في المبادئ الحالية وجهاز التنقل. استند خيار التنفيذ الأول للمنسقين إلى "جهاز توجيه" مشترك ، يملكه ويديره UINavigationController . إنه يعرف كيفية التعامل مع مثيلات UIViewController كل ما هو مطلوب فيما يتعلق بالملاحة - الضغط / البوب ​​، الحاضر / الاستبعاد بالإضافة إلى التلاعب مع وحدة تحكم الجذر . مثال على واجهة جهاز التوجيه هذا:

 import UIKit protocol Router { func present(_ module: UIViewController, animated: Bool) func dismissModule(animated: Bool, completion: (() -> Void)?) func push(_ module: UIViewController, animated: Bool, completion: (() -> Void)?) func popModule(animated: Bool) func setAsRoot(_ module: UIViewController) func popToRootModule(animated: Bool) } 

تتم تهيئة تطبيق معين بمثيل UINavigationController ولا يحتوي على أي شيء صعب بشكل خاص في حد ذاته. القيد الوحيد: لا يمكنك تمرير المثيلات الأخرى لـ UINavigationController الواجهة (لأسباب واضحة: لا يمكن أن يحتوي UINavigationController على UINavigationController في UINavigationController - وهذا تقييد UIKit ).

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

 class Coordinator { private var childCoordinators = [Coordinator]() func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } } 

واحدة من المزايا الضمنية للمنسقين هي تغليف المعرفة حول فئات فرعية محددة من UIViewController . لضمان تفاعل جهاز التوجيه والمنسقين ، قدمنا ​​الواجهة التالية:

 protocol Presentable { func presented() -> UIViewController } 

بعد ذلك يجب أن يرث كل منسق محدد من Coordinator وأن ينفذ الواجهة Presentable ، ويجب أن تأخذ واجهة جهاز التوجيه النموذج التالي:

 protocol Router { func present(_ module: Presentable, animated: Bool) func dismissModule(animated: Bool, completion: (() -> Void)?) func push(_ module: Presentable, animated: Bool, completion: (() -> Void)?) func popModule(animated: Bool) func setAsRoot(_ module: Presentable) func popToRootModule(animated: Bool) } 

(يتيح لك الأسلوب مع Presentable أيضًا استخدام المنسقين داخل الوحدات النمطية التي تتم كتابتها للتفاعل مباشرة مع مثيلات UIViewController ، دون UIViewController (الوحدات النمطية) للمعالجة الجذرية.)

مثال موجز عن كل هذا في العمل:

 final class FirstCoordinator: Coordinator, Presentable { func presented() -> UIViewController { return UIViewController() } } final class SecondCoordinator: Coordinator, Presentable { func presented() -> UIViewController { return UIViewController() } } let nc = UINavigationController() let router = RouterImpl(navigationController: nc) // Router implementation. router.setAsRoot(FirstCoordinator()) router.push(SecondCoordinator(), animated: true, completion: nil) router.popToRootModule(animated: true) 

التقريب التالي


ثم في أحد الأيام ، جاءت اللحظة لتعديل كامل للملاحة وحرية التعبير المطلقة! اللحظة التي لا يمنعنا فيها أي شيء من محاولة تطبيق التنقل على المنسقين باستخدام طريقة start() المرغوبة - وهي نسخة جذبت في الأصل بساطتها ودقتها.

من الواضح أن ميزات Coordinator المذكورة أعلاه لن تكون ضرورية. ولكن يجب إضافة نفس الطريقة إلى الواجهة العامة:

 protocol Coordinator { func add(dependency coordinator: Coordinator) func remove(dependency coordinator: Coordinator) func start() } class BaseCoordinator: Coordinator { private var childCoordinators = [Coordinator]() func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } func start() { } } 

لا تقدم "سويفت" القدرة على إعلان الطبقات المجردة (نظرًا لأنها أكثر توجهاً نحو النهج الموجه نحو البروتوكول من النهج الكلاسيكي والموجّه نحو الكائن ) ، وبالتالي يمكن ترك طريقة start() بتطبيق فارغ أو دفع هناك شيء مثل fatalError(_:file:line:) (إجبار على تجاوز هذه الطريقة مع الورثة). أنا شخصياً أفضل الخيار الأول.

لكن لدى Swift فرصة كبيرة لإضافة أساليب التنفيذ الافتراضية إلى أساليب البروتوكول ، لذلك لم تكن الفكرة الأولى بالطبع هي إعلان فئة أساسية ، ولكن للقيام بشيء مثل هذا:

 extension Coordinator { func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } } 

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

أساس أي منسق معين سيبدو كما يلي:

 final class SomeCoordinator: BaseCoordinator { override func start() { // ... } } 

يمكن إضافة أي تبعيات ضرورية للمنسق إلى المُهيئ. كحالة نموذجية ، مثيل UINavigationController .

إذا كان هذا هو منسق الجذر الذي تتمثل مسؤوليته في تعيين الجذر UIViewController ، فيمكن للمنسق ، على سبيل المثال ، قبول مثيل جديد من UINavigationController مع مكدس فارغ.

عند معالجة الأحداث (المزيد حول ذلك لاحقًا) ، يمكن للمنسق تمرير UINavigationController هذا UINavigationController إضافي إلى المنسقين الآخرين الذين UINavigationController . ويمكنهم أيضًا أن يتعاملوا مع الحالة الحالية للملاحة بما يحتاجون إليه: "الدفع" ، "الحاضر" ، وعلى الأقل استبدال رصة التنقل بأكملها.

تحسينات واجهة ممكن


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

 protocol CoordinatorDependencies { func add(dependency coordinator: Coordinator) func remove(dependency coordinator: Coordinator) } final class DefaultCoordinatorDependencies: CoordinatorDependencies { private let dependencies = [Coordinator]() func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } } final class SomeCoordinator: Coordinator { private let dependencies: CoordinatorDependencies init(dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { dependencies = dependenciesManager } func start() { // ... } } 

التعامل مع الأحداث التي أنشأها المستخدم


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

افترض أن هناك فئة فرعية من UIViewController :

 final class SomeViewController: UIViewController { } 

والمنسق الذي يضيفها إلى المكدس:

 final class SomeCoordinator: Coordinator { private let dependencies: CoordinatorDependencies private weak var navigationController: UINavigationController? init(navigationController: UINavigationController, dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { self.navigationController = navigationController dependencies = dependenciesManager } func start() { let vc = SomeViewController() navigationController?.pushViewController(vc, animated: true) } } 

نقوم بتفويض معالجة أحداث وحدة التحكم المقابلة إلى المنسق نفسه. هنا ، في الواقع ، يتم استخدام المخطط الكلاسيكي:

 protocol SomeViewControllerRoute: class { func onSomeEvent() } final class SomeViewController: UIViewController { private weak var route: SomeViewControllerRoute? init(route: SomeViewControllerRoute) { self.route = route super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @IBAction private func buttonAction() { route?.onSomeEvent() } } final class SomeCoordinator: Coordinator { private let dependencies: CoordinatorDependencies private weak var navigationController: UINavigationController? init(navigationController: UINavigationController, dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { self.navigationController = navigationController dependencies = dependenciesManager } func start() { let vc = SomeViewController(route: self) navigationController?.pushViewController(vc, animated: true) } } extension SomeCoordinator: SomeViewControllerRoute { func onSomeEvent() { // ... } } 

التعامل مع زر العودة


تم نشر مراجعة جيدة أخرى للقالب المعماري الذي تمت مناقشته بواسطة Paul Hudson على موقعه الإلكتروني "Hacking with Swift" ، حتى يمكن للمرء أن يقول دليلاً. يحتوي أيضًا على شرح بسيط ومباشر لأحد الحلول الممكنة لمشكلة زر الإرجاع المذكورة أعلاه: يعلن المنسق (إذا لزم الأمر) عن نفسه مفوضًا لمثيل UINavigationController الذي تم UINavigationController إليه ومراقبة الحدث الذي يهمنا.

يحتوي هذا الأسلوب على عيب صغير: يمكن فقط أن يكون NSObject UINavigationController مفوض UINavigationController .

لذلك ، هناك منسق يولد منسق آخر. هذا الآخر ، عن طريق استدعاء start() يضيف نوعًا من UINavigationController مكدس UINavigationController . من خلال النقر على زر الرجوع على UINavigationBar كل ما عليك القيام به هو UINavigationBar المنسق الأصلي بأن المنسق الذي تم إنشاؤه قد أنهى عمله ("التدفق"). للقيام بذلك ، قدمنا ​​أداة تفويض أخرى: يتم تخصيص مفوض لكل منسق تم إنشاؤه ، ويتم تنفيذ الواجهة الخاصة به بواسطة منسق الإنشاء:

 protocol CoordinatorFlowListener: class { func onFlowFinished(coordinator: Coordinator) } final class MainCoordinator: NSObject, Coordinator { private let dependencies: CoordinatorDependencies private let navigationController: UINavigationController init(navigationController: UINavigationController, dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { self.navigationController = navigationController dependencies = dependenciesManager super.init() } func start() { let someCoordinator = SomeCoordinator(navigationController: navigationController, flowListener: self) dependencies.add(someCoordinator) someCoordinator.start() } } extension MainCoordinator: CoordinatorFlowListener { func onFlowFinished(coordinator: Coordinator) { dependencies.remove(coordinator) // ... } } final class SomeCoordinator: NSObject, Coordinator { private weak var flowListener: CoordinatorFlowListener? private weak var navigationController: UINavigationController? init(navigationController: UINavigationController, flowListener: CoordinatorFlowListener) { self.navigationController = navigationController self.flowListener = flowListener } func start() { // ... } } extension SomeCoordinator: UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { guard let fromVC = navigationController.transitionCoordinator?.viewController(forKey: .from) else { return } if navigationController.viewControllers.contains(fromVC) { return } if fromVC is SomeViewController { flowListener?.onFlowFinished(coordinator: self) } } } 

في المثال أعلاه ، لا يقوم MainCoordinator بأي شيء: إنه ببساطة يطلق تدفق منسق آخر - في الحياة الواقعية ، بالطبع ، لا فائدة منه. في طلبنا ، يستقبل MainCoordinator البيانات من الخارج ، والتي بموجبها تحدد الحالة التي يكون فيها التطبيق - مخول وغير مصرح به ، إلخ. - وأي شاشة تحتاج إلى أن تظهر. بناءً على ذلك ، يتم تشغيل تدفق المنسق المقابل. إذا كان المنسق الأصلي قد أنهى عمله ، يتلقى المنسق الرئيسي إشارة حول ذلك من خلال CoordinatorFlowListener ويقوم ، على سبيل المثال ، بإطلاق تدفق لمنسق آخر.

استنتاج


الحل المعتاد ، بالطبع ، لديه عدد من العيوب (مثل أي حل لأي مشكلة).

نعم ، يجب عليك استخدام الكثير من التفويض ، لكنه بسيط وله اتجاه واحد: من الناتج إلى الذي تم إنشاؤه (من وحدة التحكم إلى المنسق ، ومن المنسق الذي تم إنشاؤه إلى المنسق).

نعم ، من أجل الهروب من تسرب الذاكرة ، يجب عليك إضافة طريقة تفويض UINavigationController مع تنفيذ مماثل تقريبا لكل منسق. (لا يحتوي النهج الأول على هذا العيب ، ولكن بدلاً من ذلك يشارك بسخاء معرفته الداخلية حول تعيين منسق معين.)

لكن أكبر عيب في هذا النهج هو أنه في الحياة الواقعية ، فإن المنسقين ، لسوء الحظ ، سوف يعرفون أكثر قليلاً حول العالم من حولهم مما نود. بتعبير أدق ، سيتعين عليهم إضافة عناصر منطقية تعتمد على الظروف الخارجية ، والتي لا يعرفها المنسق بشكل مباشر. في الأساس ، هذا هو في الواقع ما يحدث عندما يتم onFlowFinished(coordinator:) أسلوب start() أو onFlowFinished(coordinator:) callback. وأي شيء يمكن أن يحدث في هذه الأماكن ، وسيكون دائمًا سلوكًا "مضغوطًا": إضافة وحدة تحكم إلى المكدس ، واستبدال المكدس ، والعودة إلى وحدة تحكم الجذر - أيا كان. وكل هذا لا يعتمد على كفاءات وحدة التحكم الحالية ، ولكن على الظروف الخارجية.

ومع ذلك ، فإن الكود "جميل" وموجز ، من الجيد حقًا العمل به ، والتنقل عبر الكود أسهل كثيرًا. بدا لنا أنه مع وجود أوجه القصور المذكورة ، مع إدراكها ، من الممكن وجودها.
شكرا لقراءة هذا المكان! آمل أن يكونوا قد تعلموا شيئًا مفيدًا لأنفسهم. وإذا كنت تريد فجأة "أكثر مني" ، فإليك رابط لتويتر الخاص بي .

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


All Articles