В своём докладе я расскажу о подходе к построению навигации в больших приложениях на примере демо-проекта, приближенного по архитектуре к приложению Avito.
Данная архитектура позволяет поддерживать DeepLink’и и iPad в существующем приложении, показывать плашки Push-уведомлений в верхнем видимом модуле, совершать переходы из верхнего видимого модуля, вызванные корневым модулем приложения, а также централизовано управлять анимацией переходов.
Разработанная нами навигация оборачивает UIKit для совершения переходов в засахаренном и декларативном стиле. Использование этой системы позволило снизить связанность кода для повышения его переиспользуемости и тестируемости, а также выработать общие стандарты к построению стека навигации. Наиболее хорошо эта система сочетается с архитектурой VIPER, но может использоваться и без нее.
По ходу доклада я также расскажу о том, как выглядела навигация в приложении Avito раньше; о том, какой она стала с началом использования архитектуры VIPER; о проблемах, которые мы увидели, и об итоговом решении. Также вкратце рассмотрим, что же скрывается под капотом у этой системы.
3. Введем понятие Router
Как будет проходить презентация?
Рассмотрим необычные задачи Routing’а
Попробуем написать Router
Адаптируем Router под iPad
Сформулируем общие архитектурные правила Routing’а
Посмотрим демо
10. Пробуем написать Router c Assembly
protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: RouterProtocol {
weak var navigationController: UINavigationController?
weak var rootViewController: UIViewController?
let assemblyFactory: AssemblyFactory
func showAuthorization() {
let authorizationController = AuthorizationViewController()
let router = AuthorizationRouterImpl()
router.navigationController = navigationController
router.rootViewController = authorizationController
authorizationController.router = router
navigationController?.pushViewController(authorizationController, animated: true)
}
}
protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: RouterProtocol {
weak var navigationController: UINavigationController?
weak var rootViewController: UIViewController?
let assemblyFactory: AssemblyFactory
func showAuthorization() {
let authorizationController = AuthorizationViewController()
let router = AuthorizationRouterImpl()
router.navigationController = navigationController
router.rootViewController = authorizationController
authorizationController.router = router
navigationController?.pushViewController(authorizationController, animated: true)
}
}
protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: RouterProtocol {
weak var navigationController: UINavigationController?
weak var rootViewController: UIViewController?
let assemblyFactory: AssemblyFactory
func showAuthorization() {
if let navigationController = navigationController {
let authorizationAssembly = assemblyFactory.authorizationAssembly()
let authorizationController = authorizationAssembly.module(navigationController)
navigationController.pushViewController(authorizationController, animated: true)
}
}
}
protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: RouterProtocol {
weak var navigationController: UINavigationController?
weak var rootViewController: UIViewController?
let assemblyFactory: AssemblyFactory
func showAuthorization() {
if let navigationController = navigationController {
let authorizationAssembly = assemblyFactory.authorizationAssembly()
let authorizationController = authorizationAssembly.module(navigationController)
navigationController.pushViewController(authorizationController, animated: true)
}
}
}
protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: RouterProtocol {
weak var navigationController: UINavigationController?
weak var rootViewController: UIViewController?
let assemblyFactory: AssemblyFactory
func showAuthorization() {
if let navigationController = navigationController {
let authorizationAssembly = assemblyFactory.authorizationAssembly()
let authorizationController = authorizationAssembly.module(navigationController)
navigationController.pushViewController(authorizationController, animated: true)
}
}
}
protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: RouterProtocol {
weak var navigationController: UINavigationController?
weak var rootViewController: UIViewController?
let assemblyFactory: AssemblyFactory
func showAuthorization() {
if let navigationController = navigationController {
let authorizationAssembly = assemblyFactory.authorizationAssembly()
let authorizationController = authorizationAssembly.module(navigationController)
navigationController.pushViewController(authorizationController, animated: true)
}
}
}
protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: RouterProtocol {
weak var navigationController: UINavigationController?
weak var rootViewController: UIViewController?
let assemblyFactory: AssemblyFactory
func showAuthorization() {
if let navigationController = navigationController {
let authorizationAssembly = assemblyFactory.authorizationAssembly()
let authorizationController = authorizationAssembly.module(navigationController)
navigationController.pushViewController(authorizationController, animated: true)
}
}
}
protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: RouterProtocol {
weak var navigationController: UINavigationController?
weak var rootViewController: UIViewController?
let assemblyFactory: AssemblyFactory
func showAuthorization() {
if let navigationController = navigationController {
let authorizationAssembly = assemblyFactory.authorizationAssembly()
let authorizationController = authorizationAssembly.module(navigationController)
navigationController.pushViewController(authorizationController, animated: true)
}
}
}
protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: RouterProtocol {
weak var navigationController: UINavigationController?
weak var rootViewController: UIViewController?
let assemblyFactory: AssemblyFactory
func showAuthorization() {
if let navigationController = navigationController {
let authorizationAssembly = assemblyFactory.authorizationAssembly()
let authorizationController = authorizationAssembly.module(navigationController)
navigationController.pushViewController(authorizationController, animated: true)
}
}
}
Добавим
базовый класс
11. protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: RouterProtocol {
weak var navigationController: UINavigationController?
weak var rootViewController: UIViewController?
let assemblyFactory: AssemblyFactory
func showAuthorization() {
if let navigationController = navigationController {
let authorizationAssembly = assemblyFactory.authorizationAssembly()
let authorizationController = authorizationAssembly.module(navigationController)
navigationController.pushViewController(authorizationController, animated: true)
}
}
}
Пробуем написать Router c Assembly
Добавим
базовый класс
Пробуем написать Router с базовым классом
protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: BaseRouter, RouterProtocol {
func showAuthorization() {
if let navigationController = navigationController {
let authorizationAssembly = assemblyFactory.authorizationAssembly()
let authorizationController = authorizationAssembly.module(navigationController)
navigationController.pushViewController(authorizationController, animated: true)
}
}
}
protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: BaseRouter, RouterProtocol {
func showAuthorization() {
if let navigationController = navigationController {
let authorizationAssembly = assemblyFactory.authorizationAssembly()
let authorizationController = authorizationAssembly.module(navigationController)
navigationController.pushViewController(authorizationController, animated: true)
}
}
}
protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: BaseRouter, RouterProtocol {
func showAuthorization() {
if let navigationController = navigationController {
let authorizationAssembly = assemblyFactory.authorizationAssembly()
let authorizationController = authorizationAssembly.module(navigationController)
navigationController.pushViewController(authorizationController, animated: true)
}
}
}
Вынесем в базовый
класс
12. protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: BaseRouter, RouterProtocol {
func showAuthorization() {
if let navigationController = navigationController {
let authorizationAssembly = assemblyFactory.authorizationAssembly()
let authorizationController = authorizationAssembly.module(navigationController)
navigationController.pushViewController(authorizationController, animated: true)
}
}
}
Вынесем в базовый
класс
Пробуем написать Router с базовым классом
protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: BaseRouter, RouterProtocol {
func showAuthorization() {
pushViewControllerDerivedFrom { navigationController -> UIViewController in
let authorizationAssembly = assemblyFactory.authorizationAssembly()
let authorizationController = authorizationAssembly.module(navigationController)
return authorizationController
}
}
}
protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: BaseRouter, RouterProtocol {
func showAuthorization() {
pushViewControllerDerivedFrom { navigationController -> UIViewController in
let authorizationAssembly = assemblyFactory.authorizationAssembly()
let authorizationController = authorizationAssembly.module(navigationController)
return authorizationController
}
}
}
protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: BaseRouter, RouterProtocol {
func showAuthorization() {
pushViewControllerDerivedFrom { navigationController -> UIViewController in
let authorizationAssembly = assemblyFactory.authorizationAssembly()
let authorizationController = authorizationAssembly.module(navigationController)
return authorizationController
}
}
}
protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: BaseRouter, RouterProtocol {
func showAuthorization() {
pushViewControllerDerivedFrom { navigationController -> UIViewController in
let authorizationAssembly = assemblyFactory.authorizationAssembly()
let authorizationController = authorizationAssembly.module(navigationController)
return authorizationController
}
}
}
protocol RouterProtocol: class {
func showAuthorization()
}
final class RouterProtocolImpl: BaseRouter, RouterProtocol {
func showAuthorization() {
presentModalViewControllerDerivedFrom { navigationController -> UIViewController in
let authorizationAssembly = assemblyFactory.authorizationAssembly()
let authorizationController = authorizationAssembly.module(navigationController)
return authorizationController
}
}
}
Хороший фасад
13. Базовый класс
class BaseRouter {
weak var navigationController: UINavigationController?
weak var rootViewController: UIViewController?
func pushViewControllerDerivedFrom(deriveViewController: UINavigationController -> UIViewController) {
if let navigationController = navigationController {
let viewController = deriveViewController(navigationController)
navigationController.pushViewController(viewController, animated: true)
}
}
}
class BaseRouter {
weak var navigationController: UINavigationController?
weak var rootViewController: UIViewController?
func pushViewControllerDerivedFrom(deriveViewController: UINavigationController -> UIViewController) {
if let navigationController = navigationController {
let viewController = deriveViewController(navigationController)
navigationController.pushViewController(viewController, animated: true)
}
}
}
Что делать
с Master-detail модулем?
Для Master-detail нужен второй
навигационный контроллер
14. Второй базовый класс
class BaseMasterDetailRouter {
weak var masterNavigationController: UINavigationController?
weak var detailNavigationController: UINavigationController?
weak var rootViewController: UIViewController?
func pushMasterViewControllerDerivedFrom(deriveViewController: UINavigationController -> UIViewController) {
if let navigationController = masterNavigationController {
let viewController = deriveViewController(navigationController)
navigationController.pushViewController(viewController, animated: true)
}
}
}
class BaseMasterDetailRouter {
weak var masterNavigationController: UINavigationController?
weak var detailNavigationController: UINavigationController?
weak var rootViewController: UIViewController?
func pushMasterViewControllerDerivedFrom(deriveViewController: UINavigationController -> UIViewController) {
if let navigationController = masterNavigationController {
let viewController = deriveViewController(navigationController)
navigationController.pushViewController(viewController, animated: true)
}
}
}
class BaseMasterDetailRouter {
weak var masterNavigationController: UINavigationController?
weak var detailNavigationController: UINavigationController?
weak var rootViewController: UIViewController?
func pushMasterViewControllerDerivedFrom(deriveViewController: UINavigationController -> UIViewController) {
if let navigationController = masterNavigationController {
let viewController = deriveViewController(navigationController)
navigationController.pushViewController(viewController, animated: true)
}
}
}
class BaseMasterDetailRouter {
weak var masterNavigationController: UINavigationController?
weak var detailNavigationController: UINavigationController?
weak var rootViewController: UIViewController?
func pushMasterViewControllerDerivedFrom(deriveViewController: UINavigationController -> UIViewController) {
if let navigationController = masterNavigationController {
let viewController = deriveViewController(navigationController)
navigationController.pushViewController(viewController, animated: true)
}
}
}
class BaseMasterDetailRouter {
weak var masterNavigationController: UINavigationController?
weak var detailNavigationController: UINavigationController?
weak var rootViewController: UIViewController?
func pushMasterViewControllerDerivedFrom(deriveViewController: UINavigationController -> UIViewController) {
if let navigationController = masterNavigationController {
let viewController = deriveViewController(navigationController)
navigationController.pushViewController(viewController, animated: true)
}
}
}
class BaseMasterDetailRouter {
weak var masterNavigationController: UINavigationController?
weak var detailNavigationController: UINavigationController?
weak var rootViewController: UIViewController?
func pushMasterViewControllerDerivedFrom(deriveViewController: UINavigationController -> UIViewController) {
if let navigationController = masterNavigationController {
let viewController = deriveViewController(navigationController)
navigationController.pushViewController(viewController, animated: true)
}
}
}
Добавим структурку
для передачи
всех нужных роутеру
параметров
15. Добавляем структурки
struct RouterSeed {
let navigationController: UINavigationController
}
struct RouterSeed {
let navigationController: UINavigationController
}
struct MasterDetailRouterSeed {
let masterNavigationController: UINavigationController
let detailNavigationController: UINavigationController
}
Теперь рефакторить
будет удобней
16. Улучшенный фасад
pushViewControllerDerivedFrom { routerSeed -> UIViewController inpushViewControllerDerivedFrom { routerSeed -> UIViewController in
pushMasterViewControllerDerivedFrom { routerSeed -> UIViewController in
pushViewControllerDerivedFrom { routerSeed -> UIViewController in
pushMasterViewControllerDerivedFrom { routerSeed -> UIViewController in
setDetailViewControllerDerivedFrom { routerSeed -> UIViewController in
pushViewControllerDerivedFrom { routerSeed -> UIViewController in
pushMasterViewControllerDerivedFrom { routerSeed -> UIViewController in
setDetailViewControllerDerivedFrom { routerSeed -> UIViewController in
presentModalNavigationControllerWithRootViewControllerDerivedFrom { routerSeed -> UIViewController in
pushViewControllerDerivedFrom { routerSeed -> UIViewController in
pushMasterViewControllerDerivedFrom { routerSeed -> UIViewController in
setDetailViewControllerDerivedFrom { routerSeed -> UIViewController in
presentModalNavigationControllerWithRootViewControllerDerivedFrom { routerSeed -> UIViewController in
presentPopoverWithNavigationControllerFromBarButtonItem(buttonItem) { routerSeed -> UIViewController in
17. So far, so good
Модуль авторизации из всех модулей
Поддержка DeepLinks
Bonus: (Push’ы, Alert’ы)
Нужно научить
базовые роутеры
искать верхний модуль
So far, so good, но что если
18. Поиск верхнего модуля
protocol TopViewControllerFinder: class {
func topViewController() -> UIViewController?
}
final class TopViewControllerFinderImpl: TopViewControllerFinder {
weak var rootViewController: UIViewController?
}
final class TopViewControllerFinderImpl: TopViewControllerFinder {
weak var rootViewController: UIViewController?
func topViewController() -> UIViewController? {
var result = rootViewController
while let presentedViewController = result?.presentedViewController {
result = presentedViewController
}
return result
}
}
final class TopViewControllerFinderImpl: TopViewControllerFinder {
weak var rootViewController: UIViewController?
func topViewController() -> UIViewController? {
var result = rootViewController
while let presentedViewController = result?.presentedViewController {
result = presentedViewController
}
return result
}
}
final class TopViewControllerFinderImpl: TopViewControllerFinder {
weak var rootViewController: UIViewController?
func topViewController() -> UIViewController? {
var result = rootViewController
while let presentedViewController = result?.presentedViewController {
result = presentedViewController
}
if let selectedTabController = (result as? UITabBarController)?.selectedViewController {
if let detailController = (selectedTabController as? UISplitViewController)?.viewControllers.last {
if let detailNavigationController = detailController as? UINavigationController {
result = detailNavigationController.viewControllers.last
} else {
result = detailController
}
} else {
result = selectedTabController
}
}
return result
}
}
final class TopViewControllerFinderImpl: TopViewControllerFinder {
weak var rootViewController: UIViewController?
func topViewController() -> UIViewController? {
var result = rootViewController
while let presentedViewController = result?.presentedViewController {
result = presentedViewController
}
if let selectedTabController = (result as? UITabBarController)?.selectedViewController {
if let detailController = (selectedTabController as? UISplitViewController)?.viewControllers.last {
if let detailNavigationController = detailController as? UINavigationController {
result = detailNavigationController.viewControllers.last
} else {
result = detailController
}
} else {
result = selectedTabController
}
}
return result
}
}
Нужна своя
система навигации
19. Зачем нужна своя система навигации?
Хранение истории переходов
Поддержка Third-party контроллеров
Bonus: проверка, что модуль был на экране
Bonus: расстояние между модулями
Поиск верхнего модуля
Обертка над UIKit
Реакция на изменение SDK
20. Свежий взгляд на базовый Router
class BaseRouter {
weak var navigationController: UINavigationController?
weak var rootViewController: UIViewController?
}
class BaseRouter {
weak var navigationController: UINavigationController?
weak var rootViewController: UIViewController?
}
Нужно абстрагировать Router
от UIKit
Не у каждого роутера будет UINavigationController
Код вида .pushViewController() сильно завязывает Router на UIKit
Для каждого ThirdPartyNavigationController нужна будет своя пара базовых Router’ов
21. Абстрагируем Router от UIKit
protocol TransitionsHandler: class {
}
typealias TransitionId = String Идентификатор
перехода
Возвращение на модуль
Закрытие модуля
Отменяемый переход
protocol TransitionsHandler: class {
func performTransition(context context: PresentationTransitionContext)
}
Неотменяемый переход
protocol TransitionsHandler: class {
func performTransition(context context: PresentationTransitionContext)
func resetWithTransition(context context: ResettingTransitionContext)
}
protocol TransitionsHandler: class {
func performTransition(context context: PresentationTransitionContext)
func resetWithTransition(context context: ResettingTransitionContext)
func undoTransitionsAfter(transitionId transitionId: TransitionId)
}
protocol TransitionsHandler: class {
func performTransition(context context: PresentationTransitionContext)
func resetWithTransition(context context: ResettingTransitionContext)
func undoTransitionsAfter(transitionId transitionId: TransitionId)
func undoTransitionWith(transitionId transitionId: TransitionId)
}
Router
общается с
обработчиком переходов
23. class BaseRouter {
weak var navigationController: UINavigationController?
weak var rootViewController: UIViewController?
}
Новый базовый Router
class BaseRouter {
let transitionsHandlerBox: TransitionsHandlerBox // weak var navigationController: UINavigationController?
let transitionId: TransitionId // weak var rootViewController: UIViewController?
}
class BaseRouter {
let transitionsHandlerBox: TransitionsHandlerBox // weak var navigationController: UINavigationController?
let transitionId: TransitionId // weak var rootViewController: UIViewController?
}
class BaseRouter {
let transitionsHandlerBox: TransitionsHandlerBox // weak var navigationController: UINavigationController?
let transitionId: TransitionId // weak var rootViewController: UIViewController?
}
class BaseRouter {
let transitionsHandlerBox: TransitionsHandlerBox // weak var navigationController: UINavigationController?
let transitionId: TransitionId // weak var rootViewController: UIViewController?
}
enum TransitionsHandlerBox {
case Animating(AnimatingTransitionsHandlerImpl)
case Containing(ContainingTransitionsHandlerImpl)
}
Такой Router
можно использовать
с любым UIViewController’ом
24. Схема выполнения отменяемых переходов
Transitions handler
box
выполни
отменяемый
переход
Router
presentation
context
transitions
handler
box Transitions
Coordinator
Top animating
transitions handlerзапусти
анимацию
presentation
context
25. Взглянем еще раз на новый базовый Router
class BaseRouter {
let transitionsHandlerBox: TransitionsHandlerBox // weak var navigationController: UINavigationController?
let transitionId: TransitionId // weak var rootViewController: UIViewController?
}
Нужна ссылка
на обработчика переходов,
показавшего модуль Router’а
class BaseRouter {
let transitionsHandlerBox: TransitionsHandlerBox // weak var navigationController: UINavigationController?
let transitionId: TransitionId // weak var rootViewController: UIViewController?
weak var presentingTransitionsHandler: TransitionsHandler?
}
class BaseRouter {
let transitionsHandlerBox: TransitionsHandlerBox // weak var navigationController: UINavigationController?
let transitionId: TransitionId // weak var rootViewController: UIViewController?
weak var presentingTransitionsHandler: TransitionsHandler?
}
Теперь роутер может
закрывать свой модуль
26. Навигационная связь
Router
1
transition id
1
Transitions handler
1
Transitions handler
2
presenting
transitions
handler
Вернись на
модуль 1
Закрой
модуль 2
Router
2
transition id
2
Что лучше:
“Вернись на модуль 1”
или
“Закрой модуль 2”
?
31. Слой Router
Предварительные итоги
Подходы к выполнению обратных переходов
Поддержка DeepLinks
Слой Assembly
Базовые классы Router, поддержка iPad, master-detail
Простой Router (фасад, абстрация от UIKit, поддержка Third-Party)
33. Один UIViewController, много Router’ов
Выводы по демо
Проверка наличия модуля в истории (Авторизация)
Проверка isIpad()
Поиск верхнего модуля (Авторизация, DeepLink’и, Push’ы)
Проверка модулей на дубликаты (🍌, 🍏)
Аниматоры переходов
Проверка isIpad()
34. Выделите слой Router (определять стиль перехода)
Общие советы
Используйте “Вернись на модуль” вместо “Закрой модуль”
Выделите слой Assembly (верьте в появление DI для Swift)
Абстрагируйте Router от UIKit
Вынесите логику принятия решений в отдельный слой
Описывайте переходы в декларативном стиле
35. One more thing
https://github.com/avito-tech/Marshroute
Исходники Докладчик: Юсипов Тимур
https://vk.com/ma3tsa
tyusipov@avito.ru
ykylele@gmail.com
fizmatchelskype
personal mail
work mail
vk
Marshroute
Спасибо за внимание!
presentation.finish()
https://github.com/avito-tech/Marshroute/tree/master/Example
Демо