A Swift Kit that helps you with Screen (UIViewController
) creation, navigation, and organization into reusable coherent flows using the MVVM
pattern in combination with the Coordinator
pattern.
MVVMCoordinatorKit.mov
This Kit aims to speed up your development and help you organize Screens into coherent flows that are easily reusable using the Coordinator
pattern, making navigation between screens simple and readable.
This Kit also helps you create UIViewController
(the View
in the MVVM
pattern) and its ViewModel
.
Model
is not part of this Kit, as it is up to the developers to define their models in the app.
NOTE: This README is not meant to describe the ins and outs of the MVVM
pattern and the evolution from MVC
to MVVM
. There are lots of articles online about the MVVM
and why it is better than MVC
. If you are reading this README, it is likely that you are already familiar with the MVVM
and just want a framework that helps you with MVVM
development.
- iOS 11.0+
- Swift 5.0+
MVVMCoordinatorKit is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'MVVMCoordinatorKit'
The Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the swift
compiler.
Search for this package in Xcode's Package Dependencies and add it:
https://github.com/Dino4674/MVVMCoordinatorKit
To run the example project, clone the repo, and open Example/MVVMCoordinatorKit.xcworkspace
.
In a classic MVVM
pattern:
M
stands forModel
V
stands forView
VM
stands forViewModel
Since Apple forces us through their APIs to use the MVC
pattern (yes, talking about the UIViewController
), we are used to naming our custom ViewControllers with the ViewController suffix, which does not fit well with the MVVM
naming conventions. We want to treat UIViewController
as the Screen
.
UIView
in iOS represents a view that is part of a UIViewController
's view hierarchy, and we want to treat UIViewController
as the "main" view -> Screen
.
Since our Screen
is a UIViewController
, this Kit uses different naming conventions for the View
part of the MVVM
:
View
->Screen
ViewModel
->ScreenModel
We can call our MVVM
the MSSM
(Model-Screen-ScreenModel).
This is to distinguish between the UIViewController
and UIView
file names because in your apps, you are likely to have lots of custom UIView
s, and you are almost certainly going to append View suffix to those custom views. Additionally, when creating a UIViewController
, you are likely to name it with the ViewController suffix, which, as mentioned, does not fit well with the MVVM
naming conventions. This Kit encourages the usage of the Screen suffix for UIViewController
s.
Encapsulates a particular flow of screens and its business logic, with the ability to push/present/setRoot child coordinators.
Coordinator
has a ResultType
type which is used to notify its parent Coordinator
when it is finished with its flow.
It also has a DeepLinkType
, which you can use to implement deep linking specifically to your app needs. Note that each Coordinator
that you create in a "coordinators tree" will have to have the exact same concrete implementation of DeepLinkType
(In 99.99% of cases, this will be an enum
named DeepLinkOption
).
Has a reference to UINavigationController
and handles navigation logic (push/pop/present/dismiss/setRoot/popToRoot). Each Coordinator
has a reference to one (and only one) Router
.
A base UIViewController
with its ScreenModel
. Each Screen
is defined with its ScreenModel
.
A ScreenModel
, coupled with its holding Screen
. Each ScreenModel
defines its Result
type, which is used to notify the Coordinator
in charge when it produces results worthy of navigation changes.
MVVMCoordinatorKit is designed NOT to depend on any particular bindings implementation. The Example app uses Combine
for bindings between the Screen
and its ScreenModel
. You can use Combine
or any other of your preferred bindings implementation.
To reduce the time when creating a particular Screen + ScreenModel
or Coordinator
, you can download custom templates made for this Kit. Move the root extracted folder MVVMCoordinatorKit
into one of these two folders:
~/Library/Developer/Xcode/Templates
/Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File Templates
NOTE: If you add templates to the 2nd location, they won't survive the Xcode update.
With these templates, you can create files more quickly without the need for adding boilerplate code every time.
There are templates for Screen + ScreenModel
and for Coordinator
.
Additionally, when using the Screen + ScreenModel
template you can pick a View type:
- Code
- creates a
*Module Name*Screen.swift
WITHOUT a.xib
file (plus the*Module Name*ScreenModel.swift
).
- creates a
- With XIB
- creates a
*Module Name*Screen.swift
WITH a companion.xib
file (plus the*Module Name*ScreenModel.swift
).
- creates a
Optionally select whether to import Combine
framework with an example code serving you as a starter point for your Screen
and ScreenModel
.
In the screenshots example above, the template will generate these 3 files:
The best way to explore MVVMCoordinatorKit is to examine the Example app, which contains all the examples.
Each Coordinator
has its own Router
, which you use to do all the push/pop/present/dismiss calls. However, the BaseCoordinator
class has convenience functions for push
, present
, and setRoot
Coordinator
, which automatically handles the release of resources for you when Screen
is removed from the view stack. It doesn't matter how the Screen
is removed, all cases are supported for resources autorelease:
- Pushed
Coordinator
- Back button from
UINavigationController
- interactive left-screen-edge pan pop gesture
- manual call to
router.popModule
- Back button from
- Presented
Coordinator
- interactive top-to-bottom pan dismiss gesture
- manual call to
router.dismissModule
public func pushCoordinator(_ coordinator: BaseCoordinator, deepLink: DeepLinkType? = nil, animated: Bool = true, onPop: RouterCompletion? = nil)
public func presentCoordinator(_ coordinator: BaseCoordinator, deepLink: DeepLinkType? = nil, animated: Bool = true, onDismiss: RouterCompletion? = nil)
public func setRootCoordinator(_ coordinator: BaseCoordinator, deepLink: DeepLinkType? = nil, animated: Bool = true, onPop: RouterCompletion? = nil)
Typically if we want to PRESENT a flow, we would create a new Router
with a new UINavigationController
:
let navigationController = UINavigationController()
let router = Router(navigationController: navigationController)
let coordinator = ExampleCoordinator(router: router)
presentCoordinator(coordinator)
If we want to PUSH a flow, we will use the same Router
from the current Coordinator
:
let coordinator = ExampleCoordinator(router: router)
pushCoordinator(coordinator)
When a Coordinator
adds a child Coordinator
(push, present, doesn't matter), it will need to observe its child results, and it is doing it through this callback:
public var finishFlow: ((ResultType) -> ())?
ResultType
is defined in the Coordinator
class, and it will most likely be some enum
.
The Coordinator
template will autogenerate this enum
for you, ready to be filled with your use cases:
e.g.
enum ProfileCoordinatorResult {
case didLogout
}
class ProfileCoordinator: Coordinator<DeepLinkOption, ProfileCoordinatorResult>
let coordinator = ProfileCoordinator(router: router)
coordinator.finishFlow = { [weak self] result in
switch result {
case .didLogout: // do something here...
// push or present another flow,
// or call self?.finishFlow to propagate the event up the tree and let the parent Coordinator decide what to do next
break
}
}
pushCoordinator(coordinator)
Each Screen
needs to define its ScreenModel
.
e.g.
class ProfileScreen: Screen<ProfileScreenModel>
The Screen
class has two convenience functions for instantiating a Screen
:
public static func createWithNib(screenModel: T) -> Self // Screen with .xib file
public static func create(screenModel: T) -> Self // in-code Screen
If we are using the Code template:
let screenModel = ProfileScreenModel()
let screen = ProfileScreen.create(screenModel: screenModel)
If we are using the With XIB template:
let screenModel = ProfileScreenModel()
let screen = ProfileScreen.createWithNib(screenModel: screenModel)
ScreenModel
needs to define its Result
type (can be Void
if not needed), which is needed for its parent Coordinator
for results observation:
ProfileScreenModel<MostLikelySomeEnum>
The Screen + ScreenModel
template will autogenerate this enum
for you, ready to be filled with your use cases:
e.g.
extension ProfileScreenModel {
enum Result {
case didLogout
}
}
class ProfileScreenModel: ScreenModel<ProfileScreenModel.Result>
A parent Coordinator
, which sets up the ScreenModel
, will listen for ScreenModel
's results through this callback:
public var onResult: ((Result) -> Void)?
e.g.
// We are in ProfileCoordinator here
let screenModel = ProfileScreenModel()
screenModel.onResult = { [weak self] result in
switch result {
case .didLogout: self?.finishFlow?(.didLogout)
}
}
Since this Kit does not depend on any particular bindings implementation, it is up to you which one you want to use. The Screen + ScreenModel
template will generate two empty struct
s inside ScreenModel
, which you can use for the Screen
's input
and output
:
Input
- fill with all possible inputs from the view (e.g.
buttonTap
,swipeGestureActivated
, or any other...)
- fill with all possible inputs from the view (e.g.
Output
- fill with all possible outputs for the view (e.g.
loginButtonTitle
,screenTitle
,actionButtonEnabled
, or any other...)
- fill with all possible outputs for the view (e.g.
e.g. (Using Combine
)
extension ProfileScreenModel: ScreenModelType {
struct Input {
let logout: PassthroughSubject<Void, Never>
}
struct Output {
let screenTitle: AnyPublisher<String?, Never>
let logoutButtonTitle: AnyPublisher<String?, Never>
}
}
A quick note on logging in this Kit. There is an MVVMCoordinatorKitLogger
class used for debugging this Kit to make sure all resources are properly released.
Logging is disabled by default. You can enable it by calling this (e.g. from AppDelegate
):
MVVMCoordinatorKitLogger.loggingEnabled = true
You probably won't need this, as it will just pollute your logs.
This Kit was made possible by a bunch of other people who explored and wrote about Coordinators. This Kit is a combination of ideas from those people. It is not meant to be perfect, and it will always have space for improvement. Feel free to write suggestions, feature requests, pull requests, or just say Hi!
https://khanlou.com/2015/01/the-coordinator/
https://khanlou.com/2015/10/coordinators-redux/
When the first problems arrived (handling Screen removal from the view stack, UINavigationController's back button, and screen-edge gesture):
https://khanlou.com/2017/05/back-buttons-and-coordinators/
https://hackernoon.com/coordinators-routers-and-back-buttons-c58b021b32a
MVVMCoordinatorKit also has an upgrade for interactive dismissal of presented UIViewController
(sheet) and autorelease of resources, which the original Router
solution did not support. Additionally, MVVMCoordinatorKit has an upgrade for setRootModule
and its completion block, which is used for the autorelease of resources.
When somebody put all this together nicely (these two articles and hackernoon article above are the biggest inspiration for MVVMCoordinatorKit):
https://medium.com/blacklane-engineering/coordinators-essential-tutorial-part-i-376c836e9ba7
https://medium.com/blacklane-engineering/coordinators-essential-tutorial-part-ii-b5ab3eb4a74
Honorable mentions (not many ideas taken from these two articles, but still a good read if you want to explore the Coordinator pattern):
https://www.hackingwithswift.com/articles/71/how-to-use-the-coordinator-pattern-in-ios-apps
https://www.hackingwithswift.com/articles/175/advanced-coordinator-pattern-tutorial-ios
Dino Bartosak, [email protected]
MVVMCoordinatorKit is available under the MIT license. See the LICENSE file for more info.