Функциональный способ описания UI навигации.
SetWindowRootFlow(
in: window,
configuration: .combine(.keyAndVisible, .animated(duration: 0.3)),
DeferredBuild(MainTabBarController.init, with: { rootController in
SetTabBarItemsFlow(
in: rootController,
configuration: .titlePositionAdjustment(UIOffset(horizontal: 0.0, vertical: -4.0)),
items: [
DeferredBuild(UINavigationController.init, with: { navigationController in
PushFlow(
in: navigationController: navigationController,
animated: false,
configuration: .title("Feed"),
FeedViewController(
searchFlow: PushFlow(
in: navigationController,
SearchViewController()
),
itemFlow: { item in
PushFlow(
in: navigationController,
configuration: .combine(
.title(item.name),
.hidesBottomBarWhenPushed
),
ItemDetailsViewController(
with: item,
commentsFlow: PresentFlow(
in: navigationController,
CommentsViewController(item: item)
)
)
)
}
)
)
}),
DeferredBuild(UINavigationController.init, with: { navigationController in
PushFlow(
in: navigationController,
configuration: .title("Profile"),
ProfileViewController(
settingsFlow: PushFlow(
in: navigationController,
configuration: .title("Settings"),
SettingsViewController(
saveCompletionFlow: PopFlow(in: navigationController)
)
),
logoutFlow: AuthorizationFlow
)
)
}
]
)
})
)
- Требования
- В чем смысл?
- Конфигурация Flow
- Как использовать?
- Зачем все это нужно?
- Установка
- Контакты
- Лицензия
- iOS 9.0+
- Xcode 10.0+
- Swift 5.0+
Навигация - это действие (push, flow, set, и т.д.).
Действие можно описать ввиде кложура () -> Void
. Называть его будем просто Flow
.
Теперь можем написать фабрику методов для создания Flow
. В данном фреймворке представлены следующие (базовые) методы:
PushFlow(
in: myNavigationController,
MyViewController()
)
PopFlow(in: myNavigationController)
PopFlow(
in: myNavigationController,
to: secondViewControlller
)
PopToRootFlow(in: myNavigationController)
Present(
in: rootViewController,
MyModalViewController()
)
// Ищет самый верхний контроллер в окне и презентит в нём.
Present(
in: window,
MyModalViewController()
)
DismissFlow(myViewController)
// Дисмисит контроллер, который презентовал указанный контроллер
DismissFlow(in: presentedViewController)
// Дисмисит самой верхний контроллер в окне
DismissFlow(in: window)
SetTabBarItemsFlow(
in: tabBarController,
items: [
FeedViewController(),
ProfileViewController(),
]
)
SetWindowRootFlow(
in: window,
myRootViewController
)
Если этого кажется недостаточным, вы можете написать свою функцию аналогичным образом через глобальные функции возвращающие Flow (т.е. кложур). Например, флоу с пушем и удалением предыдущего экрана может быть реализован так:
PushReplacingFlow(
in: navigationController,
MyViewController()
)
func PushReplacingFlow(
in navigationController: UINavigationController,
animated: Bool = true,
_ viewController: @autoclosure @escaping UIViewController
) -> Flow {
return {
var newStack = navigationController.viewControllers
newStack.removeLast()
newStack.append(viewController())
navigationController.setViewControllers(
viewControllers: newStack,
animated: animated
)
}
}
Каждый Flow
имеет конфигурацию FlowConfiguration
, которая запускается перед и после выполнения.
let configuration = FlowConfiguration<UINavigationController, UIViewController>(
prepare: { navigationController, viewController in
navigationController.setNavigationBarHidden(true, animated: false)
},
completion: { navigationController, viewController in
Analytics.track(.openScreen(String(description: viewController)))
}
)
return PushFlow(
in: navigationController,
configuration: configuration,
SubscriptionViewController()
)
PushFlowConfiguration
== FlowConfiguration<UINavigationController, UIViewController>
hidesBottomBarWhenPushed
title(String?)
titleView(UIView?)
navigationDelegate(UINavigationControllerDelegate)
PresentFlowConfiguration
== FlowConfiguration<UIViewController, UIViewController>
transitionStyle(UIModalTransitionStyle)
presentationStyle(UIModalPresentationStyle)
modalInPresentation
transitionDelegate(UIViewControllerTransitioningDelegate)
SetTabBarItemsFlowConfiguration
== FlowConfiguration<UITabBarController, UIViewController>
titlePositionAdjustment(UIOffset)
SetWindowRootFlowConfiguration
== FlowConfiguration<UIWindow, UIViewController>
keyAndVisible
animated(duration: TimeInterval, completion: Flow?)
Презентуемые контроллеры инициализируются только при запуске Flow
!
Push/Present/Set/...Flow
принимают () -> ViewController
билдер.
Для каждого представленного
Flow
есть альтернативная инициализация с@autoclosure
билдером.
// контроллер еще не проинициализирован
let flow = PushFlow(
in: myNavigationController,
MyViewController()
)
// контроллер проинициализирован и запушен
flow()
Иногда может возникнуть кейс с бесконечной вложенностью стэка экранов. Например, экран перехода на страницу профиля друга из экрана твоего профиля, а из экрана профиля друга, на экран профиля его друга.
Реализовать такое с FunctionalNavigationFlowKit можно двумя способами.\
- Вынести флоу профиля в отдельную переменную/функцию и вызывать ее внутри себя(рекурсивно) при открытии профиля друга.
- Использовать
RecursiveFlow
:
RecursiveFlow(with: myUserInfo, { (userInfo: UserInfo, profileFlow: (UserInfo) -> Flow) in
PushFlow(
in: navigationController,
ProfileViewController(
userInfo: userInfo,
friendFlow: profileFlow // принимает кложур (UserInfo) -> Flow
)
}
Входной точкой приложения является AppDelegate, потому Flow
запускается оттуда.
Рассмотрим пример стандартного приложения с авторизацией:
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
if session.isAuthorized {
MainFlow(
in: window,
logoutFlow: AuthFlow
)()
} else {
AuthFlow(
in: window,
completionFlow: MainFlow
)()
}
return true
}
Пример создания MainFlow:
func MainFlow(
in window: UIWindow,
logoutFlow: @escaping Flow
) -> Flow {
// Some shared dependencies
let imageLoader = ImageLoader()
return SetWindowRootFlow(
in: window,
configuration: .makeKeyAndVisible
DeferredBuild(UINavigationViewController.init) { navigationController in
PushFlow(
in: navigationController,
FeedViewController(
feedDetailsFlow: { (feed: Feed) in
PushFlow(in: navigationController, FeedDetailsViewController(feed, imageLoader))
},
profileFlow: (userInfo) -> Flow in
PresentFlow(
in: navigationController,
ProfileViewController(
userInfo,
logoutFlow: logoutFlow
)
)
)
)
}
)
}
- Подобный подход позволяет декларативно описать карту навигации, скрывая детали презентации и сборки экрана.
- Делает независимым навигацию от конкретного экрана. Иными словами, презентуемый и презентующий контроллер не знают (и не должны знать), как будут отображаться в иерархии окон. Такой подход позволяет легко поменять тип презентации и контекст, в котором презентуется экран, без необходимости изменять что-то несвязанное с навигацией...open/closed principle (пересекается с пунктом 1).
- В
swift
можно писать вложенные функции. Учитывая то, что при работе с навигацией важно иметь доступ ко всей иерархии из любого презентуемого контекста приложения, вложенные функциии являются удобным решением для описания какого-то внутреннего скоупа навигации. Имея доступ к внешним(глобальном для внутренней фунции) переменменным, пропадает необходимость использования constructor/property/method injection из ООП, которая часто используется вCoordinator
-ах при передаче зависимостей в дочерние(n-ой вложенности) координаторы через дерево/цепочку вызовов, приводящих к созданию транзитивных зависимостей. - Глубокая вложенность может показаться чем-то плохим, но в данном случае она описывает столь же глубокую вложенность навигации приложения. Можно вынести что-то в отдельные переменную, но пользы от этого меньше, чем вреда: теряешь доступ ко всей иерархии контроллеров (нижестоящих). Исключением для вынесения может разве что наличие нескольких точек перехода на один и тот же
Flow
.
# Podfile
use_frameworks!
target 'YOUR_TARGET_NAME' do
pod 'FunctionalNavigationFlowKit'
end
Create a Package.swift
file.
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "YOUR_PROJECT_NAME",
dependencies: [
.package(url: "https://github.com/Ernest0-Production/FunctionalNavigationFlowKit.git", from: "0.0.2")
],
targets: [
.target(name: "YOUR_TARGET_NAME", dependencies: ["FunctionalNavigationFlowKit"])
]
)
FunctionalNavigationFlowKit is released under the MIT license. See LICENSE for details.