منسق التطبيق في تطبيقات iOS

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

يمكن استخدام ApplicationCoordinator لبناء التنقل بين الشاشات ، وفي نفس الوقت حل عدد من المشاكل. تحت عرض القط وتعليمات التنفيذ السريع لهذا النهج.



عن المتحدث: يعمل Pavel Gurov على تطوير تطبيقات iOS في Avito.



التنقل





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



الطرق الأكثر شيوعًا لبناء بنية تطبيقات iOS: MVc و MVVm و MVp ، تصف كيفية بناء وحدة شاشة واحدة. تقول أيضًا أن الوحدات النمطية يمكن أن تعرف عن بعضها البعض ، والتواصل مع بعضها البعض ، إلخ. ولكن يتم إيلاء القليل من الاهتمام لقضايا كيفية إجراء التحولات بين هذه الوحدات ، ومن يقرر هذه التحولات ، وكيف يتم نقل البيانات.

UlStoryboard + مقاطع


يوفر iOS خارج الصندوق عدة طرق لإظهار سيناريو الشاشة التالي:

  1. يميز UlStoryboard + المشهور ، عندما نحدد جميع الانتقالات بين الشاشات في ملف تعريف واحد ، ثم نسميها. كل شيء مريح للغاية ورائع.
  2. حاويات - مثل UINavigationController. UITabBarController أو UIPageController أو ربما حاويات مكتوبة ذاتيًا يمكن استخدامها برمجيًا مع StoryBoards.
  3. الأسلوب الحالي (_: متحرك: إكمال :). هذه مجرد طريقة لفئة UIController.

لا توجد مشاكل مع هذه الأدوات نفسها. المشكلة هي بالضبط كيف يتم استخدامها بشكل شائع. طريقة UINavigationController ، و PerformSegue ، و PreparForSegue ، و PresentViewController ، كلها طرق ملكية لفئة UIViewController. تقترح Apple استخدام هذه الأدوات داخل UIViewController نفسها.



والدليل على ذلك ما يلي.



هذه هي التعليقات التي تظهر في مشروعك إذا قمت بإنشاء فئة فرعية جديدة لـ UIViewController باستخدام قالب قياسي. إنه مكتوب مباشرة - إذا كنت تستخدم مقاطع وتحتاج إلى نقل البيانات إلى الشاشة التالية وفقًا للسيناريو ، فيجب عليك: الحصول على ViewController من segig ؛ تعرف على نوعه ؛ إرساله إلى هذا النوع وتمرير بياناتك هناك.

هذا النهج لمشاكل البناء والملاحة.

1. اتصال جامد للشاشات

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

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

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



2. إعادة ترتيب وحدات تحكم البرنامج النصي

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



3. نقل البيانات حسب السيناريو

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



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

كما يقولون ، من الأفضل أن ترى مرة واحدة من أن تسمع 100 مرة. دعونا نلقي نظرة على مثال محدد من تطبيق Avito Services Pro هذا. هذا التطبيق مخصص للمحترفين في قطاع الخدمات ، حيث يسهل تتبع طلباتك والتواصل مع العملاء والبحث عن الطلبات الجديدة.

السيناريو - اختيار مدينة لتحرير ملف تعريف المستخدم.



هنا شاشة تحرير ملف التعريف ، كما هو الحال في العديد من التطبيقات. نحن مهتمون باختيار المدينة.

ما الذي يحدث هنا؟

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

في هذا الرسم التخطيطي ، تظهر مشاكل الاتصال التي ذكرتها سابقًا على شكل أسهم بين ViewController. سنتخلص من هذه المشاكل الآن.

كيف نفعل هذا؟

  1. نحن نمنع أنفسنا داخل UIViewController من الوصول إلى الحاويات ، أي self.navigationController أو self.tabBarController أو بعض الحاويات المخصصة الأخرى التي قمت بها كملحق الملكية. الآن لا يمكننا أخذ حاويتنا من رمز الشاشة ونطلب منها القيام بشيء ما.


  2. نحن نمنع أنفسنا داخل UIViewController لاستدعاء طريقة PerformSegue وكتابة التعليمات البرمجية في طريقة PreparForSegue ، والتي ستأخذ الشاشة التي تتبع النص البرمجي وتكوينه. أي أننا لم نعد نعمل مع المقطع (مع التحولات بين الشاشات) داخل UIViewController.


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




منسق


نظرًا لأننا نزيل جميع هذه المسؤوليات من UIViewController ، فنحن بحاجة إلى كيان جديد يقوم بتنفيذها. قم بإنشاء فئة جديدة من الكائنات ، واسميها المنسق.



المنسق هو مجرد كائن عادي نمرر إليه في بداية NavigationController وندعو طريقة البدء. الآن لا تفكر في كيفية تنفيذها ، فقط انظر كيف سيتغير سيناريو اختيار المدينة في هذه الحالة.

الآن لا يبدأ بحقيقة أننا نستعد للانتقال إلى أي شاشة NavigationController محددة ، ولكننا نسمي طريقة البدء في المنسق ، ونمررها قبل ذلك في مُهيئ NavigationController. يفهم المنسق أن الوقت قد حان ليقوم NavigationController بإطلاق الشاشة الأولى ، وهو ما يفعله.

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

بعد ذلك ، ينقر المستخدم على "المنطقة" - نفس الصورة بالضبط - الشاشة نفسها لا تحل أي شيء ، فقط تخبر المنسق أن الشاشة التالية تفتح.

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

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



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

في هذه الحالة ، نستخدم المنسق لسحب منطق الانتقالات إلى الطبقة أعلاه وإزالة هذه المعرفة من ViewController.

تجريبي


يتوفر عرض تقديمي ومشروع تجريبي على Github ، فيما يلي عرض توضيحي أثناء الحديث.


هذا هو نفس السيناريو: تحرير ملف تعريف واختيار مدينة فيه.

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

دعونا نرى الآن كيف يعمل هذا في التعليمات البرمجية. لنبدأ بالنموذج.

struct City { let name: String } struct User { let name: String var city: City? } 

النماذج بسيطة:

  1. هيكل مدينة له اسم حقل ، سلسلة ؛
  2. مستخدم لديه أيضًا اسم ومدينة عقار.

التالي هو StoryBoard . يبدأ بـ NavigationController. من حيث المبدأ ، إليك نفس الشاشات التي كانت موجودة في جهاز المحاكاة: شاشة تحرير المستخدم مع تسمية وزر وشاشة بقائمة المدن ، والتي تعرض جهازًا لوحيًا به مدن.

شاشة تحرير المستخدم


 import UIKit final class UserEditViewController: UIViewController, UpdateableWithUser { // MARK: - Input - var user: User? { didSet { updateView() } } // MARK: - Output - var onSelectCity: (() -> Void)? @IBOutlet private weak var userLabel: UILabel? @IBAction private func selectCityTap(_ sender: UIButton) { onSelectCity?() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) updateView() } private func updateView() { userLabel?.text = "User: \(user?.name ?? ""), \n" + "City: \(user?.city?.name ?? "")" } } 

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

يحدث الشيء نفسه في طريقة viewWillAppear ().

المكان الأكثر إثارة للاهتمام هو المعالج للنقر على زر تحديد المدينة selectCityTap (). هنا ، وحدة التحكم نفسها لا تحل أي شيء : لا تنشئ أي وحدات تحكم ، ولا تستدعي أي مقطع. كل ما يفعله هو رد الاتصال - هذه هي الخاصية الثانية لـ ViewController. رد الاتصال onSelectCity لا يحتوي على معلمات. عندما ينقر المستخدم على الزر ، يؤدي ذلك إلى استدعاء هذا الاتصال.

شاشة اختيار المدينة


 import UIKit final class CitiesViewController: UITableViewController { // MARK: - Output - var onCitySelected: ((City) -> Void)? // MARK: - Private variables - private let cities: [City] = [City(name: "Moscow"), City(name: "Ulyanovsk"), City(name: "New York"), City(name: "Tokyo")] // MARK: - Table - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return cities.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) cell.textLabel?.text = cities[indexPath.row].name return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { onCitySelected?(cities[indexPath.row]) } } 

هذه الشاشة هي UITableViewController. تم إصلاح قائمة المدن هنا ، ولكنها قد تأتي من مكان آخر. علاوة على ذلك (// MARK: - Table -) هو رمز جدول تافهة إلى حد ما يعرض قائمة المدن في الخلايا.

المكان الأكثر إثارة للاهتمام هنا هو معالج didSelectRowAt IndexPath ، وهي طريقة معروفة للجميع. هنا الشاشة نفسها لا تحل أي شيء. ماذا يحدث بعد اختيار المدينة؟ يطلق عليه ببساطة استدعاء مع معلمة واحدة "مدينة".

هذا ينهي كود الشاشات نفسها. كما نرى ، لا يعرفون أي شيء عن بيئتهم.

منسق


دعنا ننتقل إلى الرابط بين هذه الشاشات.

 import UIKit protocol UpdateableWithUser: class { var user: User? { get set } } final class UserEditCoordinator { // MARK: - Properties private var user: User { didSet { updateInterfaces() } } private weak var navigationController: UINavigationController? // MARK: - Init init(user: User, navigationController: UINavigationController) { self.user = user self.navigationController = navigationController } func start() { showUserEditScreen() } // MARK: - Private implementation private func showUserEditScreen() { let controller = UIStoryboard.makeUserEditController() controller.user = user controller.onSelectCity = { [weak self] in self?.showCitiesScreen() } navigationController?.pushViewController(controller, animated: false) } private func showCitiesScreen() { let controller = UIStoryboard.makeCitiesController() controller.onCitySelected = { [weak self] city in self?.user.city = city _ = self?.navigationController?.popViewController(animated: true) } navigationController?.pushViewController(controller, animated: true) } private func updateInterfaces() { navigationController?.viewControllers.forEach { ($0 as? UpdateableWithUser)?.user = user } } } 

يحتوي المنسق على خاصيتين:

  1. المستخدم - المستخدم الذي سنقوم بتحريره ؛
  2. NavigationController الذي ستمر إليه عند بدء التشغيل.

هناك حرف (init) بسيط يملأ هذه الخاصية.

التالي هو الأسلوب start () الذي يتسبب في استدعاء الأسلوب ShowUserEditScreen () . دعونا نتناولها بمزيد من التفصيل. هذه الطريقة تخرج وحدة التحكم من UIStoryboard ، وتمررها إلى مستخدمنا المحلي. ثم يقوم بإجراء رد الاتصال SelectCity ويدفع وحدة التحكم هذه إلى مكدس التنقل.

بعد أن ينقر المستخدم على الزر ، يتم تشغيل رد الاتصال onSelectCity ، مما يؤدي إلى استدعاء طريقة ShowCitiesScreen () الخاصة التالية.

في الواقع ، إنها تفعل نفس الشيء تقريبًا - فهي ترفع وحدة تحكم مختلفة قليلاً عن UIStoryboard ، وتضع رد الاتصال onCitySelected عليها وتدفعها إلى حزمة التنقل - هذا كل ما يحدث. عندما يختار المستخدم مدينة معينة ، يتم تشغيل رد الاتصال هذا ، ويقوم المنسق بتحديث حقل "المدينة" الخاص بالمستخدم المحلي لدينا ويقوم بترحيل حزمة التنقل إلى الشاشة الأولى.

نظرًا لأن المستخدم عبارة عن هيكل ، فإن تحديث حقل "المدينة" فيه يؤدي إلى حقيقة أن كتلة didSet تسمى ، على التوالي ، يتم استدعاء تحديث الطريقة الخاصة interInfaces () . تمر هذه الطريقة عبر حزمة التنقل بالكامل وتحاول نشر كل ViewController كبروتوكول UpdateableWithUser. هذا هو أبسط بروتوكول ، له خاصية واحدة فقط - المستخدم. إذا نجح ذلك ، فإنه يلقيها إلى المستخدم المحدث. وبالتالي ، اتضح أن المستخدم الذي اخترناه على الشاشة الثانية يقفز تلقائيًا إلى الشاشة الأولى.

كل شيء واضح مع المنسق ، والشيء الوحيد المتبقي هنا هو نقطة الدخول إلى طلبنا. هذا هو المكان الذي يبدأ فيه كل شيء. في هذه الحالة ، هذه هي طريقة didFinishLaunchingWithOptions في AppDelegate.

 import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var coordinator: UserEditCoordinator! func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { guard let navigationController = window?.rootViewController as? UINavigationController else { return true } let user = User(name: "Pavel Gurov", city: City(name: "Moscow")) coordinator = UserEditCoordinator(user: user, navigationController: navigationController) coordinator.start() return true } } 

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

المدخلات والمخرجات


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



المدخل هو أي بيانات قد تتغير بمرور الوقت ، بالإضافة إلى بعض طرق ViewController التي يمكن استدعاؤها من الخارج. على سبيل المثال ، في UserEditViewController ، تعد هذه خاصية مستخدم - يمكن للمستخدم نفسه أو معلمة المدينة الخاصة به أن تتغير.

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

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

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

يمكنك التخلص من هذا بنفس الطريقة كما في التفويض ، باستخدام البروتوكولات.



لتجنب الاتصال ، يمكننا إغلاق الإدخال والإخراج لوحدة التحكم الخاصة بنا باستخدام بروتوكول .

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



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

الآن أهم شيء - لماذا تفعل كل هذا؟ لماذا نحتاج إلى البناء في مستوى إضافي آخر عندما لا تكون هناك مشاكل على أي حال؟

يمكن للمرء أن يتخيل هذا الموقف: سيأتي إلينا مدير ويطلب منك إجراء اختبار أ / ب لحقيقة أنه بدلاً من قائمة المدن ، سيكون لدينا اختيار مدينة على الخريطة. إذا لم يكن اختيار المدينة في تطبيقنا في مكان واحد ، ولكن في منسقين مختلفين ، في سيناريوهات مختلفة ، كان علينا أن نخيط علمًا في كل مكان ، ونرميه في الخارج ، على هذا العلم رفع إما واحد أو في ViewController الآخر. هذه ليست مريحة للغاية.

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

تكوين مقابل الوراثة


النقطة التالية التي أردت التركيز عليها هي التكوين ضد الميراث.



  1. الطريقة الأولى لكيفية عمل منسقنا هي جعل التكوين عندما يتم تمرير NavigationController إليه من الخارج وتخزينه محليًا كخاصية. يشبه هذا التكوين - لقد أضفنا NavigationController كخاصية إليه.
  2. من ناحية أخرى ، هناك رأي بأن كل شيء موجود في Kit UI Kit ، ولا نحتاج إلى إعادة اختراع العجلة. يمكنك ببساطة أن تأخذ وترث واجهة المستخدم NavigationController .

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

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

القصص المصورة


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

لحسن الحظ ، فإن نهج المنسق لا يحد من اختيار الأدوات . يمكننا استخدام المنسقين بأمان مع السلاسل. ولكن يجب أن نتذكر أنه لا يمكننا الآن العمل مع مقاطع داخل UIViewController.



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

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

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



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

سيصبح UserEditCoordinator الخاص بنا في هذه الحالة ببساطة أنماطًا بديلة ، ليحل محل نوع RootViewController الخاص به في المعلمة العامة.

نقل بيانات البرنامج النصي مرة أخرى


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



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

اتضح أن الإجراء على وحدة تحكم واحدة (على الثالث) يجب أن يؤدي إلى تغيير في مظهر عدة أجهزة أخرى في وقت واحد. أي أنه في البداية يجب أن نظهر في الخلية مع المدينة ، وفي الثانية يجب علينا تحديث جميع الأرقام في المناطق المحددة.

يقوم المنسق بتبسيط هذه المهمة عن طريق نقل البيانات مرة أخرى إلى البرنامج النصي - وهذه مهمة بسيطة الآن مثل نقل البيانات إلى الأمام وفقًا للبرنامج النصي.

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

إعادة هيكلة الكود الحالي


كيف يمكن إعادة بناء الشفرة الحالية إذا كنت تريد تضمين هذا النهج في تطبيق موجود يحتوي على MVc أو MVVm أو MVp؟



لديك مجموعة من ViewController. أول شيء يجب فعله هو تقسيمهم إلى سيناريوهات يشاركون فيها. في مثالنا ، هناك 3 سيناريوهات: التخويل ، تعديل الملف الشخصي ، الشريط.



نلف الآن كل سيناريو داخل منسقنا. يجب أن نكون قادرين ، في الواقع ، على بدء هذه البرامج النصية من أي مكان في تطبيقنا. يجب أن يكون هذا مرنًا - يجب أن يكون المنسق مكتفيًا ذاتيًا تمامًا .

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

بعد أن قررنا منسقينا ، نحتاج إلى تحديد السيناريو الذي يمكن أن يؤدي إلى بداية سيناريو آخر ، وتشكيل شجرة من هذه السيناريوهات.



في حالتنا ، الشجرة بسيطة: LoginCoordinator يمكن أن يبدأ منسق تحرير الملف الشخصي. هنا ، يقع كل شيء تقريبًا في مكانه ، ولكن تبقى تفاصيل مهمة جدًا - يفتقر مخططنا إلى نقطة دخول.



ستكون نقطة الدخول هذه منسقة خاصة - ApplicationCoordinator . يتم إنشاؤه وتشغيله بواسطة AppDelegate ، ثم يتحكم بالفعل في المنطق على مستوى التطبيق ، أي المنسق الذي يبدأ الآن.

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

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

من أين تبدأ


أنصحك بالبدء من الأسفل إلى الأعلى - أولاً تنفيذ النصوص الفردية.



كحل بديل ، يمكن بدء تشغيلها داخل UIViewController. أي أنه طالما لم يكن لديك Root أو منسقون آخرون ، يمكنك إنشاء منسق واحد ، وكحل مؤقت ، قم ببدء تشغيله من UIViewController ، وحفظه محليًا في الملكية (كما هو التالي nextCoordinator أعلاه). عندما يحدث حدث ، أنت ، كما بينت في العرض التوضيحي ، قم بإنشاء خاصية محلية ، وضع المنسق هناك واستدعي طريقة البدء عليه. كل شيء بسيط للغاية.

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

الملخص


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

AppsConf 2018 هو بالفعل يومي 8 و 9 أكتوبر - لا تفوّت الفرصة! بدلا من ذلك ، دراسة الجدول الزمني (أو مراجعة عليه) وحجز التذاكر. بطبيعة الحال ، يتم إيلاء الكثير من الاهتمام لكل من المنصات - iOS و Android ، بالإضافة إلى تقارير حول الهندسة المعمارية غير المرتبطة بتقنية واحدة فقط ، ومناقشة القضايا المهمة الأخرى المتعلقة بالعالم حول تطوير الأجهزة المحمولة.

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


All Articles