В предыдущей статье я рассказал о подходе который мы используем для осуществления композиции и навигации между вью контроллерами в нескольких приложениях над которыми я работаю, который, в итоге, вылился в отдельную библиотеку 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
, то будут выполнены следующие шаги:
ClassFinder
не найдетProductViewController
и роутер двинется дальшеNilFinder
никогда ничего не найдет и роутер двинется дальшеGeneralStep.current
всегда вернет самый верхнийUIViewController
в стеке.- Стартовый
UIViewController
найден, роутер повернет назад - Построит
UINavigationController
используя `NavigationControllerFactory - Покажет его модально используя
GeneralAction.presentModally
- Создаст
ProductViewController
ипользуяProductViewControllerFactory
- Интегрирует созданный
ProductViewController
в предыдущийUINavigationController
ипользуяUINavigationController.pushToNavigation
- Закончит навигацию
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 способа добиться этого в конфигурации:
- Настрить
StackIteratingFinder
искать только в видимых используя [.current, .visible] - Использовать
NilFinder
что будет означать что рутер никогда не найдет имеющийся в табахBagViewController
и всегда будет создавать его. Однако, у этого подхода есть побочный эффект — если, допустим, пользователь уже вBagViewController
е представленном модально, и, допустим, кликает на универсальную ссылку, которая должна показать емуBagViewController
— то роутер его не найдет и создаст еще один экземпляр и покажет над ним модально. Это, возможно, не то что вы хотите - Изменить немного
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 и т.д.)
Буду рад вашим комментариям и предложениям. Особенно если вы считаете, что на каких то аспектах стоит остановиться подробнее. Возможно концепция контекстов требует пояснения.
6eromKYcIIexy
спасибо за статью! от души
spiceginger Автор
Всегда пожалуйста. Если будут вопросы — задавайте