Розробка

Приклади конфігурації RouteComposer

У попередній статті я розповів про підході який ми використовуємо для здійснення композиції і переходів між в’ю контролерами в декількох додатках над якими я працюю, який, у підсумку, вилився в окрему бібліотеку 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 вже присутня в стеку. (Так наприкладGeneralStep.current() гарантовано присутня в стеку в’ю контролерів) Тоді роутер почне рухатися назад по ланцюжку кроків створюючи необхідні UIViewControllerи використовуючи надані Fabricта інтегруючи їх використовуючи зазначені Actionи. Завдяки перевірці типів ще на етапі компіляції, частіше за все, ви не зможете використовувати Actionи несумісні з наданою Fabricой (тобто не зможете використовувати UITabBarController.addTab під в’ю контролер побудований NavigationControllerFactory).

 

Якщо уявити описану вище конфігурацію, то у випадку якщо у вас на екрані, просто якийсь не ProductViewController, будуть виконані наступні кроки:

 

  1. ClassFinder не знайде ProductViewController і роутер рушить далі
  2. NilFinder ніколи нічого не знайде і роутер рушить далі
  3. GeneralStep.current завжди поверне самий верхній UIViewController в стеку.
  4. Стартовий UIViewController знайдений, роутер поверне назад
  5. Побудує UINavigationController використовуючи `NavigationControllerFactory
  6. Покаже його модально використовуючи GeneralAction.presentModally
  7. Створить ProductViewController іпользуя ProductViewControllerFactory
  8. Інтегрує створений ProductViewController в попередній UINavigationController іпользуя UINavigationController.pushToNavigation
  9. Закінчить навігацію

 

NB: Слід розуміти, що в реальності не можна показати модально UINavigationController без якогось UIViewController всередині нього. Тому кроки 5-8 будуть виконані роутером трохи в іншому порядку. Але про це не слід замислюватися. Описується конфігурація послідовно.

 

Хорошою практикою при написанні конфігурації є припущення, що користувач в даний момент може перебувати де завгодно в вашому додатку, і, раптом, отримує push-повідомлення з вимогою потрапити на екран який ви описуєте, і спробувати відповісти на питання — “Як має повестися додаток?”, “Як поведуть себе Finderи в конфігурації яку я описую?”. Якщо всі ці питання враховані — ви отримуєте конфігурацію яка гарантовано покаже користувачеві необхідний екран де б він не знаходився. А це головна вимога до сучасних програм з боку команд займаються маркетингом і залученням (энгейджментом) користувачів.

 

StackIteratingFinder і його опції:

 

Ви можете реалізувати концепцію Finderа будь-яким способом який вважаєте найбільш прийнятним. Однак, найбільш простим є ітерація по графу в’ю контролерів на екрані. Для спрощення цієї мети бібліотека надає StackIteratingFinder і різні реалізації які візьмуть цю задачу на себе. Вам же залишиться тільки відповісти на питання — чи це UIViewController який ви очікуєте.

 

Для того що б вплинути на поведінку StackIteratingFinder і повідомити йому в яких частинах графа (стека) в’ю контролерів ви хочете, що б він шукав, при його створенні можна вказати комбінацію SearchOptions. І на них варто зупинитися докладніше:

 

  • current: Самий верхній в’ю контролер в стеку. (Той що є rootViewController у UIWindow або той який показаний модально на самому верху)
  • visible: У тому випадку, якщо UIViewController є контейнером — шукати в його видимих UIViewControllerах (Наприклад: у UINavigationController завжди є один видимий UIViewController, у UISplitController їх може бути одна або дві залежно від того, як він представлений.)
  • contained: У тому випадку, якщо UIViewController є контейнером — шукати у всіх його вложеных UIViewControllerах (Наприклад: Пройтися по всім в’ю контролерам UINavigationController включаючи видимий)
  • presenting: Шукати також у всіх UIViewControllerах під самим верхнім (якщо вони є звичайно)
  • presented: Шукати у UIViewControllerах над наданими (для StackIteratingFinder ця опція не має сенсу, так як він завжди починає з самого верхнього)

 

Наступний малюнок можливо зробить вище більш наочним:

 

Я б рекомендував ознайомитися з концепцією контейнерів в попередній статті.

 

Приклад Якщо ви хочете, що б ваш Finder шукав AccountViewController у всьому стеку але тільки серед видимих UIViewControllerів то це слід записати так:

 

ClassFinder<AccountViewController, Any?>(options: [.current, .visible, .presenting])

 

NB Якщо по якійсь причині наданих налаштувань буде мало — ви завжди зможете легко написати свою реалізацію Finderа. Один з прикладів буде і в цій статті

 

Перейдемо, власне, до прикладів.

 

Приклади конфігурацій з поясненнями

 

У мене є певний UIViewController, який є rootViewControllerом UIWindow, і я хочу, щоб після закінчення навігації він замінився на якийсь HomeViewController:

 

 let screen = StepAssembly(
 finder: ClassFinder<HomeViewController, Any?>(),
 factory: XibFactory())
.using(GeneralAction.replaceRoot())
.from(GeneralStep.root())
.assemble()

 

XibFactory завантажить HomeViewController з xib файлу HomeViewController.xib

 

Не забудьте, що якщо ви використовуєте абстрактні реалізації Finder і Factory в комбінації, ви повинні вказати тип UIViewController і контексту як мінімум в однієї з сутностей — ClassFinder<HomeViewController, Any?>

 

Що станеться, якщо, у наведеному вище прикладі, я заміню GeneralStep.root на GeneralStep.current?

 

Конфігурація буде працювати до тих пір поки не буде викликана в той момент, коли на екрані буде будь-модальний UIViewController. У цьому випадку GeneralAction.replaceRoot не зможе замінити рутовий контролер, так як над ним є модальний, і роутер повідомить про помилку. Якщо ж ви хочете щоб ця конфігурація працювала в будь-якому випадку, то вам потрібно пояснити роутеру, що ви хочете, щоб GeneralAction.replaceRoot було застосовано саме до рутовому UIViewController. Тоді роутер прибере всі модально представлені UIViewControllerи і конфігурація відпрацює при будь-якому розкладі.

 

Я хочу показати якийсь AccountViewController, у разі якщо він ще ну показано, всередині будь-якого UINavigationControllerа який в даний момент є де небудь на екрані (навіть якщо цей UINavigationController під певним модальним UIViewControllerом):

 

 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а який в даний момент є де небудь на екрані, а якщо такого UINavigationControllerа не виявиться — створити його та показати модально:

 

 let screen = StepAssembly(
 finder: ClassFinder<AccountViewController, Any?>(),
 factory: XibFactory())
.using(UINavigationController.PushToNavigation())
 .from(SwitchAssembly<UINavigationController, Any?>()
 .addCase(expecting: ClassFinder<UINavigationController, Any?>(options: .visible)) // Якщо знайдено - працюємо від нього
 .assemble(default: { // в іншому випадку така конфігурація
 return ChainAssembly()
 .from(SingleContainerStep(finder: NilFinder(), factory: NavigationControllerFactory()))
.using(GeneralAction.presentModally())
.from(GeneralStep.current())
.assemble()
})
 ).assemble()

 

Я хочу показати 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()

 // Де потрібно в конфігурації
 .using(GeneralAction.PresentModally(transitioningDelegate: transitionController))

 

Я хочу перейти в 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 дозволяє нам уникнути помилки компіляції, гарантуючи що ми подбали, що коли роутер дійде до loginScreen в рантайме — UINavigationController там буде.

 

Що відбудеться, якщо в конфігурації вище я заміню GeneralStep.current на GeneralStep.root?

 

Вона буде працювати, але так як ви кажете роутеру, що хочете щоб він почав будувати ланцюжок від рутового UIViewController, то, якщо над ним будуть відкриті якісь модальні UIViewControllerи, роутер приховає їх перед тим як почати будувати ланцюжок.

 

У моїй програмі є UITabBarController містить HomeViewController і BagViewController як табів. Я хочу, щоб користувач міг між ними перемикатися використовуючи іконки на табах як зазвичай. Але якщо я викличу конфігурація програмно (Наприклад користувач натисне “Go to Bag” всередині HomeViewController), додаток повинен не перемкнути таб, а показати BagViewController модально.

 

Тут 3 способи домогтися цього в конфігурації:

 

  1. Настрить StackIteratingFinder шукати лише у видимих використовуючи [.current, .visible]
  2. Використовувати NilFinder що буде означати що рутер ніколи не знайде наявний у табах BagViewControllerі завжди буде створювати його. Однак, у цього підходу є побічний ефект — якщо, припустимо, користувач вже в BagViewControllerе представленому модально, і, припустимо, клікає на універсальну посилання, яка повинна показати йому BagViewController — то роутер його не знайде і створить ще один екземпляр і покаже над ним модально. Це, можливо, не то що ви хочете
  3. Змінити трохи ClassFinder щоб він знаходив тільки 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х додатках і ще не зіткнулися з ситуацією, де б він був не достатньо гнучким. Бібліотека, як і надана їй реалізація роутера, не використовує ніяких трюків з objective c рантаймом і повністю відповідає всім концепціям Cocoa Touch, лише допомагаючи розбити процес композиції на кроки і виконує їх в заданій послідовності і протестована з версіями iOS з 9 по 12. Крім того, даний підхід вписується у всі архітектурний патерни які передбачають роботу з `UIViewController` стеком (MVC, MVVM, VIP, RIB, VIPER і т. д.)

Буду радий вашим коментарям та пропозиціям. Особливо якщо ви вважаєте, що на якихось аспектах варто зупинитися докладніше. Можливо концепція контекстів вимагає пояснення.

Related Articles

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

Close