Flow integration, based on the concepts of FlowControllers, Coordinators and ResponderChain.
enum MyRoute: RouteType {
// MARK: - Routes
case login
case homeTabBar(tab: HomeTab)
// MARK: - RouteType
var transition: RouteTransition {
switch self {
case .login: return .set
case .homeTabBar: return .set
}
}
}
// Create a router
let rootNavigationController = UINavigationController()
let router = Router<MyRoute>(navigationController: rootNavigationController)
// Navigate to a route using enums
router.navigate(to: MyRoute.login)
// Navigate to a route using URL's
router.openURL(url)
This is the part where you implement the `integration` related stuff, such as:
- Delegates / closures / etc, for the callbacks from your controllers or actors.
- Configure the actions before each transition, i.e., register the route resolvers,
in order to have control over the flow.
class MyIntegrator: Integrator<MyRoute> {
// This function needs to be `overriden`, otherwhise, it's gonna throw a `fatalError`
override func start() {
// Implement everithing that is related to the start of the flow...
// We we suggest to set the first / starting route here.
}
override func executeBeforeTransition(to route: RouteType) throws -> UIViewController {
// Here goes the logic you need before each transition...
// Stuff like ViewController configurations and such.
}
}
You only need to do one thing to add URL support to your routes:
- register the mapping functions for the required path patterns.
class MyIntegrator: Integrator<MyRoute> {
/* ... */
func registerURLs() {
router.map("login") { _ in return .login }
router.map("home-tabbar/{tab}") { try MyRoute.homeTabBar(HomeTab($0.param("tab"))) }
}
}
Then, handle it on the AppDelegate:
extension AppDelegate {
/// Open Universal Links
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([Any]?) -> Void) -> Bool
{
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL,
let handledURL = router.openURL(url) else {
return false // Unrecognized URL
}
return handledURL
}
}
Here is an example using the popular Hero Transitions library.
Set the customTransitionDelegate
for the Router
:
router.customTransitionDelegate = self
(Optional) Define your custom transitions in an extension so you can refer to them statically
extension RouteTransition {
static var heroCrossFade: RouteTransition {
return .custom(identifier: "HeroCrossFade")
}
}
Implement the delegate method performTransition(...)
:
extension Router: RouterCustomTransitionDelegate {
/// Handle custom transitions
func performTransition(to viewController: UIViewController,
from sourceViewController: UIViewController,
transition: RouteTransition,
animated: Bool,
completion: ((Error?) -> Void)?) {
if transition == .heroCrossFade {
sourceViewController.hero.isEnabled = true
destViewController.hero.isEnabled = true
destViewController.hero.modalAnimationType = .fade
// Creates a container nav stack
let containerNavController = UINavigationController()
containerNavController.hero.isEnabled = true
containerNavController.setViewControllers([newViewController], animated: false)
// Present the hero animation
sourceViewController.present(containerNavController, animated: animated) {
completion?(nil)
}
} else {
completion?(nil)
}
}
}
And override the transition to your custom in your Router:
override func transition(for route: Route) -> RouteTransition {
switch route {
case .profile:
return .heroCrossFade
default:
return super.transition(for: route)
}
}
In order to send messages from the Parent
flow to it's Child
, you need to do this:
class MyParentIntegrator: Integrator {
/* ... */
func someThingThatNeedsToPassAMessageToTheChilds {
// Define the message, normally and enum, which needs to conform with `IntegratorInput`
let message = .something(with: someData)
// Then send it
sendInputToChild("MyChildIdentifier", input: message)
// OR
broadcastInputToAllChilds(input: message)
}
}
On the Child
, you need to override the function below in order to intercept the messages.
class MyChildIntegrator: Integrator {
/* ... */
override func receiveInput(_ input: IntegratorInput) {
switch (input) {
case .someInput(let data):
// Do something with the data
}
}
}
In order to send messages from the Child
flow to it's Parent
, you need to do this:
class MyChildIntegrator: Integrator {
/* ... */
func someThingThatNeedsToPassAMessageToTheParent {
// Define the message, normally and enum, which needs to conform with `Integrator`
let message = .something(with: someData)
// Then send it
sendOutputToParent(output: message)
}
}
On the Parent
, you need to override the function below in order to intercept the messages.
class MyParentIntegrator: Integrator {
/* ... */
func receiveOutput(from child: Integrator, output: IntegratorOutput) {
switch (child, output) {
case let (integrator as SomeIntegrator, output as SomeIntegrator.Output):
switch output {
case .someOutput:
// Do something with the message received
sendOutputToParent(output) // Then pass it on if needed, or not...
}
default: return
}
}
}
Examples and more tests (unit included)!
Thanks to the guys from XRouter, URLNavigator, RouteComposer, CoreNavigation and Compass for the resources provided that helped me to create this tool.