تكوين UIViewControllers والملاحة بينهما (وليس فقط)


في هذه المقالة ، أرغب في مشاركة التجربة التي كنا نستخدمها بنجاح لعدة سنوات في تطبيقات iOS ، 3 منها موجودة حاليًا في Appstore. نجح هذا النهج بشكل جيد وقمنا بفصله مؤخرًا عن باقي التعليمات البرمجية وتصميمه في مكتبة RouteComposer منفصلة ، والتي سيتم مناقشتها في الواقع .


https://github.com/ekazaev/route-composer


ولكن ، بالنسبة للمبتدئين ، دعنا نحاول معرفة المقصود بتكوين وحدات التحكم في العرض في iOS.


قبل الشروع في الشرح نفسه ، أذكرك أنه في iOS غالبًا ما يُفهم على أنه وحدة تحكم في العرض أو UIViewController . هذه فئة موروثة من UIViewController القياسي ، وهي وحدة التحكم الأساسية في نمط MVC التي توصي Apple باستخدامها لتطوير تطبيقات iOS.


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



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


تشتمل وحدات التحكم في عرض الحاوية القياسية المزودة بـ Cocoa Touch على: UINavigationConroller و UITabBarController و UISplitController و UIPageController وغيرها. أيضا ، يمكن للمستخدم إنشاء وحدات تحكم عرض الحاوية المخصصة الخاصة بهم باتباع قواعد Cocoa Touch الموصوفة في وثائق Apple.


عملية إدخال وحدات تحكم العرض القياسية في وحدات تحكم عرض الحاوية ، وكذلك دمج وحدات تحكم العرض في مكدس وحدات التحكم ، سوف نسمي التكوين في هذه المقالة.


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


دعونا نلقي نظرة على تكوين بعض وحدات تحكم عرض الحاوية القياسية كمثال:


أمثلة على التكوين في حاويات قياسية


UINavigationController



 let tableViewController = UITableViewController(style: .plain) //        let navigationController = UINavigationController(rootViewController: tableViewController) // ... //        let detailViewController = UIViewController(nibName: "DetailViewController", bundle: nil) navigationController.pushViewController(detailViewController, animated: true) // ... //     navigationController.popToRootViewController(animated: true) 

UITabBarController



 let firstViewController = UITableViewController(style: .plain) let secondViewController = UIViewController() //   let tabBarController = UITabBarController() //         tabBarController.viewControllers = [firstViewController, secondViewController] //        tabBarController.selectedViewController = secondViewController 

UISplitViewController



 let firstViewController = UITableViewController(style: .plain) let secondViewController = UIViewController() //   let splitViewController = UISplitViewController() //        splitViewController.viewControllers = [firstViewController] //        splitViewController.showDetailViewController(secondViewController, sender: nil) 

أمثلة على تكامل (تكوين) وحدات التحكم في العرض على المكدس


تثبيت جذر تحكم العرض


 let window: UIWindow = //... window.rootViewController = viewController window.makeKeyAndVisible() 

عرض مشروط للتحكم في العرض


 window.rootViewController.present(splitViewController, animated: animated, completion: nil) 

لماذا قررنا إنشاء مكتبة للتأليف


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


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


سيكون من الرائع استدعاء طرق مثل goToAccount() أو goToMenu() أو goToProduct(withId: "012345") عندما ينقر مستخدم على زر أو عندما goToProduct(withId: "012345") تطبيق رابط عالمي من تطبيق آخر ولا يفكر في دمج وحدة تحكم العرض هذه في المكدس ، مع العلم أن منشئ تحكم العرض هذا قد قدم هذا التنفيذ بالفعل.


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



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


يبقى أن نضيف أن كل هذا سيتم ضربه في N بمجرد أن يعرب فريق التسويق الخاص بك عن رغبته في إجراء اختبار A / B على المستخدمين المباشرين والتحقق من طريقة التنقل التي تعمل بشكل أفضل ، على سبيل المثال ، شريط علامة التبويب أو قائمة الهامبرغر؟


  • دعونا قطع ساقي سوزان دعنا نظهر 50 ٪ من مستخدمي شريط Tab ، وإلى قائمة Hamburger الأخرى ، وفي غضون شهر سنخبرك بالمستخدمين الذين يرون المزيد من عروضنا الخاصة؟

سأحاول أن أخبرك كيف تعاملنا مع حل هذه المشكلة وخصصناها أخيرًا لمكتبة RouteComposer.


سوزانين مؤلف الطريق


بعد تحليل جميع سيناريوهات التكوين والملاحة ، حاولنا تلخيص الشفرة الواردة في الأمثلة أعلاه وتحديد 3 كيانات رئيسية تعمل مكتبة RouteComposer - Factory ، Finder ، Action . بالإضافة إلى ذلك ، تحتوي المكتبة على 3 كيانات مساعدة مسؤولة عن الضبط الصغير الذي قد يكون مطلوبًا أثناء عملية التنقل - RoutingInterceptor ، PostRoutingTask ، PostRoutingTask . يجب تكوين جميع هذه الكيانات في سلسلة من التبعيات ونقلها إلى Router y ، الكائن الذي سيعمل على بناء مجموعة وحدات التحكم الخاصة بك.


ولكن ، عن كل واحد منهم بالترتيب:


مصنع


كما يوحي الاسم ، فإن Factory مسؤولة عن إنشاء وحدة تحكم العرض.


 public protocol Factory { associatedtype ViewController: UIViewController associatedtype Context func build(with context: Context) throws -> ViewController } 

هنا من المهم إبداء تحفظ على مفهوم السياق . السياق داخل المكتبة ، نسمي كل ما قد يحتاجه المشاهد من أجل إنشائه. على سبيل المثال ، لإظهار وحدة تحكم في العرض تعرض تفاصيل المنتج ، تحتاج إلى تمرير معرف منتج معين إليها ، على سبيل المثال ، في شكل String . يمكن أن يكون جوهر السياق أي شيء: كائن أو بنية أو كتلة أو مجموعة. إذا كانت وحدة التحكم الخاصة بك لا تحتاج إلى أي شيء لكي يتم إنشاؤها - هل يمكن تحديد السياق على أنه " Any? وتثبيت في nil .


على سبيل المثال:


 class ProductViewControllerFactory: Factory { func build(with productID: UUID) throws -> ProductViewController { let productViewController = ProductViewController(nibName: "ProductViewController", bundle: nil) productViewController.productID = productID //  ,      `ContextAction`,     return productViewController } } 

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


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


عمل


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


التنفيذ الأكثر شيوعًا لـ Action هو العرض التقديمي لوحدة التحكم:


 class PresentModally: Action { func perform(viewController: UIViewController, on existingController: UIViewController, animated: Bool, completion: @escaping (_: ActionResult) -> Void) { guard existingController.presentedViewController == nil else { completion(.failure("\(existingController) is already presenting a view controller.")) return } existingController.present(viewController, animated: animated, completion: { completion(.continueRouting) }) } } 

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


مكتشف


يجيب جوهر Finder جهاز التوجيه على السؤال - هل تم إنشاء وحدة التحكم هذه بالفعل وهل هي موجودة بالفعل على المكدس؟ ربما لا يوجد شيء مطلوب إنشاؤه ويكفي إظهار ما هو موجود بالفعل؟ .


 public protocol Finder { associatedtype ViewController: UIViewController associatedtype Context func findViewController(with context: Context) -> ViewController? } 

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


مثال على هذا التنفيذ:


 class ProductViewControllerFinder: StackIteratingFinder { let options: SearchOptions init(options: SearchOptions = .currentAndUp) { self.options = options } func isTarget(_ productViewController: ProductViewController, with productID: UUID) -> Bool { return productViewController.productID == productID } } 

الكيانات المساعدة


RoutingInterceptor


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


 class LoginInterceptor: RoutingInterceptor { func execute(for destination: AppDestination, completion: @escaping (_: InterceptorResult) -> Void) { guard !LoginManager.sharedInstance.isUserLoggedIn else { // ... //  LoginViewController       completion(.success)  completion(.failure("User has not been logged in.")) // ... return } completion(.success) } } 

تم تضمين تنفيذ مثل RoutingInterceptor مع التعليقات في المثال المقدم مع المكتبة.


ContextTask


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


PostRoutingTask


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


بمزيد من التفصيل مع تنفيذ جميع الكيانات الموصوفة يمكن العثور عليها في وثائق المكتبة وكذلك في المثال المرفق.


ملاحظة: لا يقتصر عدد الكيانات المساعدة التي يمكن إضافتها إلى التكوين.


التكوين


جميع الكيانات الموصوفة جيدة لأنها تقسم عملية التكوين إلى كتل صغيرة قابلة للتبديل وموثوقة جيدًا.


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


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


إذا قمت بتحليل هذا المثال دون استخدام مكتبة ، فسيبدو شيئًا مثل هذا:


 class ProductArrayViewController: UITableViewController { let products: [UUID]? let analyticsManager = AnalyticsManager.sharedInstance //  UITableViewControllerDelegate override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let productID = products[indexPath.row] else { return } //   LoginInterceptor guard !LoginManager.sharedInstance.isUserLoggedIn else { //    LoginViewController         `showProduct(with: productID)` return } showProduct(with: productID) } func showProduct(with productID: String) { //   ProductViewControllerFactory let productViewController = ProductViewController(nibName: "ProductViewController", bundle: nil) //   ProductViewControllerContextTask productViewController.productID = productID //   NavigationControllerStep  PushToNavigationAction let navigationController = UINavigationController(rootViewController: productViewController) //   GenericActions.PresentModally present(alertController, animated: navigationController) { [weak self]   . ProductViewControllerPostTask self?.analyticsManager.trackProductView(productID: productID) } } } 

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


ضع في اعتبارك تكوين هذا المثال باستخدام المكتبة:


 let productScreen = StepAssembly(finder: ProductViewControllerFinder(), factory: ProductViewControllerFactory()) //  : .adding(LoginInterceptor()) .adding(ProductViewControllerContextTask()) .adding(ProductViewControllerPostTask(analyticsManager: AnalyticsManager.sharedInstance)) //  : .using(PushToNavigationAction()) .from(NavigationControllerStep()) // NavigationControllerStep -> StepAssembly(finder: NilFinder(), factory: NavigationControllerFactory()) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble() 

إذا قمت بترجمة هذا إلى لغة بشرية:


  • تحقق من أن المستخدم قد قام بتسجيل الدخول ، وإذا لم يقدم له إدخالًا
  • إذا قام المستخدم بتسجيل الدخول بنجاح ، فتابع
  • البحث عن وحدة تحكم عرض المنتج المقدمة من Finder
  • إذا تم العثور عليه - اجعله مرئيًا وانتهى
  • إذا لم يتم العثور عليها - قم بإنشاء UINavigationController ، UINavigationController بدمجها في وحدة تحكم العرض التي تم إنشاؤها بواسطة ProductViewControllerFactory باستخدام PushToNavigationAction
  • GenericActions.PresentModally UINavigationController GenericActions.PresentModally باستخدام GenericActions.PresentModally من وحدة تحكم العرض الحالية

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


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


كائنات التكوين


 // `RoutingDestination`    .          . struct AppDestination: RoutingDestination { let finalStep: RoutingStep let context: Any? } struct Configuration { //     ,             static func productDestination(with productID: UUID) -> AppDestination { let productScreen = StepAssembly(finder: ProductViewControllerFinder(), factory: ProductViewControllerFactory()) .add(LoginInterceptor()) .add(ProductViewControllerContextTask()) .add(ProductViewControllerPostTask(analyticsManager: AnalyticsManager.sharedInstance)) .using(PushToNavigationAction()) .from(NavigationControllerStep()) .using(GenericActions.PresentModally()) .from(CurrentControllerStep()) .assemble() return AppDestination(finalStep: productScreen, context: productID) } } 


 class ProductArrayViewController: UITableViewController { let products: [UUID]? //... // DefaultRouter -  Router   ,   UIViewController   let router = DefaultRouter() override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let productID = products[indexPath.row] else { return } router.navigate(to: Configuration.productDestination(with: productID)) } } 


 @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { //... func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool { guard let productID = UniversalLinksManager.parse(url: url) else { return false } return DefaultRouter().navigate(to: Configuration.productDestination(with: productID)) == .handled } } 

.


. , , , — ProductArrayViewController, UINavigationController HomeViewController — StepAssembly from() . RouteComposer , ( ). , Configuration . , A/B , .


بدلا من الاستنتاج


, 3 . , , . Fabric , Finder Action . , — , , . , .


, , objective c Cocoa Touch, . iOS 9 12.


UIViewController (MVC, MVVM, VIP, RIB, VIPER ..)


, , , . . .


.

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


All Articles