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

كيف يوزع جهاز التوجيه التكوين
للبدء ، ضع في اعتبارك كيفية تحليل جهاز التوجيه للتكوين الذي كتبته. خذ المثال من المقالة السابقة:
let productScreen = StepAssembly(finder: ClassFinder(options: [.current, .visible]), factory: ProductViewControllerFactory()) .using(UINavigationController.pushToNavigation()) .from(SingleContainerStep(finder: NilFinder(), factory: NavigationControllerFactory())) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble()
سوف يمر جهاز التوجيه بسلسلة من الخطوات بدءًا من الخطوة الأولى ، وحتى إحدى الخطوات (باستخدام Finder
المقدم) "تفيد" أن UIViewController
المطلوب UIViewController
بالفعل على المكدس. (على سبيل المثال ، GeneralStep.current()
ضمان وجود GeneralStep.current()
في حزمة وحدة التحكم). بعد ذلك سيبدأ جهاز التوجيه في الرجوع للخلف عبر سلسلة من الخطوات لإنشاء UIViewController
المطلوبة باستخدام UIViewController
المقدمة ودمجها باستخدام Action
المحددة. بفضل التحقق من النوع حتى في مرحلة التجميع ، في معظم الأحيان ، لن تتمكن من استخدام UITabBarController.addTab
غير المتوافقة مع Fabric
المقدم (أي أنك لن تتمكن من استخدام UITabBarController.addTab
في وحدة تحكم UITabBarController.addTab
التي أنشأها NavigationControllerFactory
).
إذا كنت تتخيل التكوين الموضح أعلاه ، فعندئذٍ إذا كان لديك فقط ProductViewController
على الشاشة ، فسيتم تنفيذ الخطوات التالية:
- لن
ClassFinder
على ProductViewController
وسيستمر جهاز التوجيه - لن يجد
NilFinder
أبدًا أي شيء وسيستمر جهاز التوجيه - ستعطي
GeneralStep.current
دائمًا أعلى UIViewController
على المكدس. UIViewController
العثور على بدء UIViewController
، سيعود جهاز التوجيه- ينشئ
UINavigationController
باستخدام `NavigationControllerFactory - سيظهر بشكل مشروط باستخدام
GeneralAction.presentModally
ProductViewController
ProductViewController ProductViewControllerFactory
- يدمج
ProductViewController
تم إنشاؤه في UINavigationController
السابق باستخدام UINavigationController.pushToNavigation
- الانتهاء من التنقل
ملاحظة: يجب أن يكون من المفهوم أنه في الواقع من المستحيل إظهار UINavigationController
موحد بدون بعض UINavigationController
بداخله. لذلك ، سيتم تنفيذ الخطوات 5-8 بواسطة جهاز التوجيه بترتيب مختلف قليلاً. ولكن لا يجب التفكير في الأمر. يتم وصف التكوين بالتسلسل.
من الممارسات الجيدة عند كتابة التكوين افتراض أن المستخدم يمكن أن يكون موجودًا في أي مكان في تطبيقك في الوقت الحالي ، وفجأة ، يتلقى رسالة دفع مع طلب للوصول إلى الشاشة التي تصفها ، ومحاولة الإجابة على السؤال - "كيف يجب أن يتصرف التطبيق؟ ؟ "،" كيف يتصرف Finder
في التكوين الذي أصفه؟ ". إذا تم أخذ جميع هذه الأسئلة في الاعتبار ، فستحصل على تهيئة مضمونة لتظهر للمستخدم الشاشة المطلوبة أينما كان. وهذا هو المطلب الرئيسي للتطبيقات الحديثة من الفرق المشاركة في التسويق وجذب المستخدمين (إشراكهم) .
StackIteratingFinder
وخياراته:
يمكنك تنفيذ مفهوم Finder
بأي طريقة تعتقد أنها مقبولة للغاية. ومع ذلك ، فإن أسهل طريقة هي التكرار من خلال الرسم البياني لوحدات التحكم في العرض على الشاشة. لتبسيط هذا الهدف ، توفر المكتبة StackIteratingFinder
والتطبيقات المختلفة التي ستتولى هذه المهمة. عليك فقط الإجابة على السؤال - هل هذا هو UIViewController
الذي تتوقعه.
للتأثير على سلوك StackIteratingFinder
بأجزاء الرسم البياني (المكدس) لوحدات التحكم في العرض التي تريدها أن تبحث عنها ، يمكنك تحديد مجموعة من SearchOptions
عند إنشائها. ويجب أن يسكنوا بمزيد من التفصيل:
current
: وحدة تحكم العرض الأعلى على المكدس. (الذي هو rootViewController
UIWindow
أو الذي يظهر بشكل مشروط في الأعلى)visible
: إذا كان UIViewController
عبارة عن حاوية ، فابحث في UINavigationController
آه المرئي (على سبيل المثال: UINavigationController
يحتوي دائمًا على UINavigationController
مرئي ، قد يكون لدى UISplitController
واحدة أو اثنتين اعتمادًا على كيفية تقديمه.contained
: في حالة كون UIViewController
حاوية ، ابحث في جميع وحدات UINavigationController
المتداخلة (على سبيل المثال: UINavigationController
جميع وحدات التحكم في العرض الخاصة بـ UINavigationController
بما في ذلك وحدة التحكم المرئية)presenting
: ابحث أيضًا في كل UIViewController
آه تحت UIViewController
(إذا كان هناك بالطبع)presented
: ابحث في StackIteratingFinder
عن الخيار المقدم (بالنسبة لـ StackIteratingFinder
لا يكون هذا الخيار منطقيًا ، لأنه يبدأ دائمًا من الأعلى)
قد يوضح الشكل التالي الشرح أعلاه أكثر وضوحًا:

أوصي بالتعرف على مفهوم الحاويات في مقال سابق.
مثال إذا كنت تريد من Finder
يبحث عن AccountViewController
في المكدس بأكمله ولكن فقط بين AccountViewController
المرئي ، فيجب كتابة هذا على النحو التالي:
ClassFinder<AccountViewController, Any?>(options: [.current, .visible, .presenting])
ملحوظة: إذا كانت الإعدادات المقدمة لسبب ما قليلة ، فبإمكانك دائمًا كتابة تطبيق Finder
الخاص بك بسهولة. سيكون أحد الأمثلة في هذه المقالة.
دعونا ننتقل في الواقع إلى الأمثلة.
أمثلة على التكوينات مع التفسيرات
لدي rootViewController
معين ، وهو rootViewController
UIWindow
، وأريد HomeViewController
بـ HomeViewController
معين في نهاية التنقل:
let screen = StepAssembly( finder: ClassFinder<HomeViewController, Any?>(), factory: XibFactory()) .using(GeneralAction.replaceRoot()) .from(GeneralStep.root()) .assemble()
XibFactory
بتحميل HomeViewController
من ملف HomeViewController.xib
الخاص بـ HomeViewController.xib
لا تنس أنه إذا كنت تستخدم تطبيقات مجردة من Finder
and Factory
معًا ، فيجب عليك تحديد نوع UIViewController
والسياق لواحد على الأقل من الكيانات - ClassFinder<HomeViewController, Any?>
ماذا يحدث إذا قمت في المثال أعلاه باستبدال GeneralStep.root
بـ GeneralStep.current
؟
سيعمل التكوين حتى يتم استدعاؤه في اللحظة التي يكون فيها أي UIViewController
مشروط على الشاشة. في هذه الحالة ، لن يتمكن GeneralAction.replaceRoot
من استبدال وحدة تحكم الجذر ، نظرًا لوجود وحدة تحكم مشروطة فوقه ، وسيبلغ جهاز التوجيه عن خطأ. إذا كنت تريد أن يعمل هذا التكوين على أي حال ، فأنت بحاجة إلى أن توضح للموجه أنك تريد تطبيق GeneralAction.replaceRoot
بشكل خاص على الجذر UIViewController
. ثم سيقوم جهاز التوجيه بإزالة جميع UIViewControllers الممثلة بشكل موحد وسيعمل التكوين في أي موقف.
أريد عرض بعض AccountViewController
، إذا كان لا يزال يظهر بشكل جيد ، داخل أي UINavigationController
وهو موجود حاليًا على الشاشة في مكان ما (حتى إذا كان UINavigationController
هذا تحت بعض UINavigationController
المشروط):
let screen = StepAssembly( finder: ClassFinder<AccountViewController, Any?>(), factory: XibFactory()) .using(UINavigationController.pushToNavigation()) .from(SingleStep(ClassFinder<UINavigationController, Any?>(), NilFactory())) .from(GeneralStep.current()) .assemble()
ماذا يعني NilFactory
في هذا التكوين؟ من خلال ذلك ، تخبر جهاز التوجيه أنه إذا لم يتمكن من العثور على أي UINavigationController
على الشاشة ، فأنت لا تريده أن يقوم بإنشائه وفقط لعدم القيام بأي شيء في هذه الحالة. بالمناسبة ، نظرًا لأن هذا NilFactory
، لا يمكنك استخدام Action
بعده.
أرغب في عرض بعض AccountViewController
، إذا لم يتم عرضه بالفعل ، داخل أي UINavigationController
والذي يوجد حاليًا في مكان ما على الشاشة ، وإذا لم يكن الأمر كذلك ، قم بإنشائه وعرضه بشكل مشروط:
let screen = StepAssembly( finder: ClassFinder<AccountViewController, Any?>(), factory: XibFactory()) .using(UINavigationController.PushToNavigation()) .from(SwitchAssembly<UINavigationController, Any?>() .addCase(expecting: ClassFinder<UINavigationController, Any?>(options: .visible))
أريد أن أظهر UITabBarController
تحتوي على HomeViewController
و AccountViewController
مع استبدالها بالجذر الحالي:
let tabScreen = SingleContainerStep( finder: ClassFinder(), factory: CompleteFactoryAssembly(factory: TabBarControllerFactory()) .with(XibFactory<HomeViewController, Any?>(), using: UITabBarController.addTab()) .with(XibFactory<AccountViewController, Any?>(), using: UITabBarController.addTab()) .assemble()) .using(GeneralAction.replaceRoot()) .from(GeneralStep.root()) .assemble()
هل يمكنني استخدام UIViewControllerTransitioningDelegate
المخصص مع الإجراء GeneralAction.presentModally
:
let transitionController = CustomViewControllerTransitioningDelegate()
أريد أن أذهب إلى AccountViewController
، أينما كان المستخدم ، في علامة تبويب أخرى أو حتى في نوع من نافذة مشروطة:
let screen = StepAssembly( finder: ClassFinder<AccountViewController, Any?>(), factory: NilFactory()) .from(tabScreen) .assemble()
لماذا نستخدم NilFactory
؟ لا نحتاج إلى إنشاء AccountViewController
إذا لم يتم العثور عليه. سيتم بناؤه في تكوين tabScreen
. نراها أعلاه.
أريد أن ForgotPasswordViewController
، ولكن ، بالتأكيد ، بعد LoginViewController
داخل UINavigationController
:
let loginScreen = StepAssembly( finder: ClassFinder<LoginViewController, Any?>(), factory: XibFactory()) .using(UINavigationController.pushToNavigation()) .from(NavigationControllerStep()) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble() let forgotPasswordScreen = StepAssembly( finder: ClassFinder<ForgotPasswordViewController, Any?>(), factory: XibFactory()) .using(UINavigationController.pushToNavigation()) .from(loginScreen.expectingContainer()) .assemble()
يمكنك استخدام التهيئة في المثال للتنقل في ForgotPasswordViewController
و LoginViewController
لماذا expectingContainer
في المثال أعلاه؟
نظرًا لأن إجراء pushToNavigation
يتطلب وجود UINavigationController
وفي التكوين بعده ، تسمح لنا طريقة expectingContainer
UINavigationController
بتجنب خطأ في التجميع من خلال التأكد من أننا نحرص على أنه عندما يصل جهاز التوجيه إلى loginScreen
في loginScreen
، سيكون UINavigationController
موجودًا.
ماذا يحدث إذا قمت في التكوين أعلاه باستبدال GeneralStep.current
بـ GeneralStep.root
؟
ستعمل ، ولكن نظرًا لأنك تخبر جهاز التوجيه أنك تريده أن يبدأ في بناء سلسلة من UIViewController
الجذر ، إذا تم فتح أي UIViewController
مشروط فوقه ، فسيخفيها جهاز التوجيه قبل البدء في بناء السلسلة.
يحتوي UITabBarController
على HomeViewController
يحتوي على HomeViewController
و BagViewController
تبويب. أريد أن يتمكن المستخدم من التبديل بينها باستخدام الرموز الموجودة على علامات التبويب كالمعتاد. ولكن إذا قمت بالاتصال بالتكوين برمجيًا (على سبيل المثال ، قام المستخدم بالنقر فوق "الانتقال إلى حقيبة" داخل HomeViewController
) ، فيجب ألا يقوم التطبيق بتبديل علامة التبويب ، ولكن إظهار BagViewController
موحد.
هناك 3 طرق لتحقيق ذلك في التكوين:
- قم بتعيين
StackIteratingFinder
للبحث فقط في تلك المرئية باستخدام [.current ، .visible] - استخدم
NilFinder
مما يعني أن جهاز التوجيه لن يعثر أبدًا على BagViewController في BagViewController
وسيعمل دائمًا على إنشائه. ومع ذلك ، فإن هذا النهج له تأثير جانبي - على سبيل المثال ، إذا تم تقديم مستخدم موجود بالفعل في BagViewController
بشكل موحد ، وعلى سبيل المثال ، ينقر على رابط عالمي يجب أن BagViewController
، فلن يعثر عليه جهاز التوجيه وسيعمل على إنشاء مثيل آخر BagViewController
فوقه مشروط. قد لا يكون هذا ما تريده. - قم بتغيير
ClassFinder
الصغير بحيث يجد فقط BagViewController
معروض بشكل مشروط ويتجاهل الباقي ، BagViewController
بالفعل في التكوين.
struct ModalBagFinder: StackIteratingFinder { func isTarget(_ viewController: BagViewController, with context: Any?) -> Bool { return viewController.presentingViewController != nil } } let screen = StepAssembly( finder: ModalBagFinder(), factory: XibFactory()) .using(UINavigationController.pushToNavigation()) .from(NavigationControllerStep()) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble()
بدلا من الاستنتاج
آمل أن تكون طرق تكوين جهاز التوجيه أكثر وضوحًا إلى حد ما. كما قلت ، نستخدم هذا النهج في 3 تطبيقات ولم نواجه حتى الآن موقفًا لا يتسم بالمرونة الكافية. لا تستخدم المكتبة ، وكذلك تنفيذ جهاز التوجيه المقدم إليها ، أي حيل موضوعية مع وقت التشغيل وتتبع جميع مفاهيم Cocoa Touch بالكامل ، مما يساعد فقط على كسر عملية التكوين إلى خطوات وتنفيذها في التسلسل المحدد واختبارها مع إصدارات iOS 9 إلى 12. بالإضافة إلى ذلك ، هذا النهج يتناسب مع جميع الأنماط المعمارية التي تنطوي على العمل مع مكدس UIViewController (MVC ، MVVM ، VIP ، RIB ، VIPER ، إلخ.)
سأكون سعيدا لتعليقاتكم واقتراحاتكم. خاصة إذا كنت تعتقد أنه من المفيد التطرق إلى بعض الجوانب بمزيد من التفاصيل. ربما يحتاج مفهوم السياقات إلى توضيح.