This implementation of Coordinator pattern provides:
- Automatic
present
/dismiss
calls for modal controller presentation - Automatic
pushViewController
/popViewController
calls forUINavigationController
presentation - Automatic dismissal/popping of multiple view controllers if all of them finish simultaneously
- Automatic detection of modal iOS 13 form sheet dismissal in modal presentation
- Automatic detection of
interactivePopGestureRecognizer
dismissal inUINavigationController
- Propagation of results to the parent
- Ability to organize custom coordinators that allow for magical modification of
UINavigationController
stack or modal presentation stack - Context-independent navigation
- Automatic management of a
UIWindow
You can use it alongside any of your existing navigation solutions.
Currently, only CocoaPods is supported.
Minimum iOS version: 12.0
Minimum Xcode version: 12.0
This framework has no dependencies.
pod 'Insecurity'
We start with a "Select Payment Method" screen.
The screen will emit an event when the user presses Done button.
Once it happens, we need to close the screen.
We do this by subclassing ModalCoordinator
.
struct PaymentMethodScreenResult {
let paymentMethodChanged: Bool
}
class PaymentMethodCoordinator: ModalCoordinator<PaymentMethodScreenResult> {
override var viewController: UIViewController {
let viewController = PaymentMethodViewController()
viewController.onDone = { result in
self.finish(result)
}
return viewController
}
}
Now, when the user presses "Add Payment Method" button, we need to open the next screen.
This screen will be "Create a new payment method".
Once it's finished, we need to notify the "Select Payment Method" screen of its creation.
Here is a coordinator for AddPaymentMethod
screen.
struct PaymentMethod {
let cardNumber: String
}
class AddPaymentMethodCoordinator: ModalCoordinator<PaymentMethod> {
override var viewController: UIViewController {
let addPaymentMethodViewController = AddPaymentMethodViewController()
addPaymentMethodViewController.onPaymentMethodAdded = { paymentMethod in
self.finish(paymentMethod)
}
return addPaymentMethodViewController
}
}
And here is how we start it from PaymentMethodCoordinator
:
viewController.onNewPaymentMethodRequested = {
let addPaymentMethodCoordinator = AddPaymentMethodCoordinator()
self.navigation.start(addPaymentMethodCoordinator, animated: true) { result in
// `result` is the PaymentMethod?
}
}
Important part: in iOS 13 user can also dismiss the screen by swiping it down using a gesture.
Insecurity
framework handles this situation automatically.
The result
you receive in the code will be:
nil
if the screen is dismissed by gesturePaymentMethod
if the coordinator callsfinish
Here is the final code:
viewController.onNewPaymentMethodRequested = {
let addPaymentMethodCoordinator = AddPaymentMethodCoordinator()
self.navigation.start(addPaymentMethodCoordinator, animated: true) { [weak viewController] paymentMethod in
if let paymentMethod = paymentMethod {
// User has added a new payment method
viewController?.handleNewPaymentMethodAdded(paymentMethod)
} else {
// User dismissed the screen, nothing to do
}
}
}
⚠️ Note theweak viewController
. Be careful not to create any retain cycles.
In order to start working with Insecurity
in your app, you should create a coordinator that manages your UIWindow
.
It's called WindowCoordinator
.
Then, you call navigation.start
methods on it in order to start your instances of ModalCoordinator.
class AppCoordinator: WindowCoordinator {
func start() {
let paymentMethodCoordinator = PaymentMethodCoordinator()
self.navigation.start(paymentMethodCoordinator, duration: 0.5, options: .transitionCrossDissolve) { result in
// Payment method coordinator result
}
}
}
The duration
and options
parameters regulate that animation of UIWindow.rootViewController
change.
And here is how you start your instance of AppCoordinator
:
@main class ApplicationDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var appCoordinator: AppCoordinator?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
let window = UIWindow(frame: UIScreen.main.bounds)
self.window = window
let appCoordinator = AppCoordinator(window)
self.appCoordinator = appCoordinator
appCoordinator.start()
window.makeKeyAndVisible()
return true
}
}
⚠️ Never-ever retain theUIViewController
that you return fromviewController
⚠️ OneModalCoordinator
manages strictly oneUIViewController
. New view controller = newModalCoordinator
- Working with a
UINavigationController
- Finishing several screens at a time
- Using context-independent navigation with
AdaptiveCoordinator
- Starting on the top-most presentation context
- Writing a custom coordinator that allows for magical navigation
- Deeplinking
- Dismissing artificially
- Starting coordinator chain without
WindowCoordinator
A process of showing a screen is an inherently functional operation. A classical Coordinator Tree implementation centers around a tree of objects. Parent coordinator retains the child coordinator, and so on and so forth. This tree can be freely traversed, usually for deeplinking propagation. However, the OOP-based model of Coordinator Tree doesn't advise anything on the event propagation, nor is it concerned with the quirks of using the navigation facilities of UIKit. And with this, comes an immense variety in visions of how the navigation is supposed to be performed.
This project however models the Coordinator Tree not in an object-oriented way, but a function-oriented way. A process of presenting a screen can be represented as an asynchronous function that accepts input data and emits output data asynchronously. Making a UIWindow
key and visible is a function that returns Never
in the single-window applications. A process of presenting a UIViewController
modally is a function that returns the result of whatever the controller has settled upon. Same goes with pushing a view controller onto a UINavigationController
. And these coordinators allow you to strictly define the results of their lifecycles.
Additionally, this framework forces you to build navigation in a magic-free way. A chain of modal view controllers can be resolved and dismissed simultaneously in a transparent way, that will involve your conscious decision. And if you really need to perform some magic, you can always leave the realm of automatically handled navigation and implement custom behavior in a compatible way.
Also, this framework uses the best it can take from functional programming without explicitly relying on FRP. The public interface is still Object-Oriented and the internal components are written in an efficient imperative style programming with careful state management, thoughtful memory management and lowest footprint possible mentality.
So, all in all, no RxSwift
this time around. Though it's very compatible with RxSwift
, a Single
wrapper is very easy to write.
You will need Xcode 13 for the development.
- Clone the repo
bundle config --set path vendor/bundle
bundle install
, make sure your Xcode can be found at/Applications/Xcode.app
, otherwise the "ffi" package native extensions required by CocoaPods won't compilebundle exec pod install
open Insecurity.xcworkspace