Background:
Of course, the choice of an architectural approach affects the implementation of navigation and the organization of data transport in the project, however, the approach itself is made up of a number of circumstances: team composition, time to market, the state of the technical specification, the scalability of the project, and many others, the determining factors for me were:
- mandatory use of MVVM;
- the ability to quickly add new screens (controllers and their view models) to the navigation process;
- changes in business logic should not affect navigation;
- changes in navigation should not affect business logic;
- the ability to quickly reuse screens without making corrections to navigation;
- the ability to quickly get an idea of existing screens;
- the ability to quickly get an idea of the dependencies in the project;
- do not raise the threshold for developers to enter the project.
Get to the point
It should be noted that the final solution was not formed in one day, it is not without its drawbacks and is more suitable for small and medium-sized projects. For clarity, the test project can be viewed here: github.com/ArturRuZ/NavigationDemo
1. To be able to quickly get an idea of the existing screens, it was decided to create an enum with the unambiguous name ControllersList.
enum ControllersList {
case textInputScreen
case textConfirmationScreen
}
2. For a number of reasons, the project did not want to use third-party solutions for DI, and I wanted to get DI, including with the ability to quickly view the dependencies in the project, so it was decided to use Assembly for each separate screen (closed by the Assembly protocol) and RootAssembly as general scope.
protocol Assembly {
func build() -> UIViewController
}
final class TextInputAssembly: Assembly {
func build() -> UIViewController {
let viewModel = TextInputViewModel()
return TextInputViewController(viewModel: viewModel)
}
}
final class TextConfirmationAssembly: Assembly {
private let text: String
init(text: String) {
self.text = text
}
func build() -> UIViewController {
let viewModel = TextConfirmationViewModel(text: text)
return TextConfirmationViewController(viewModel: viewModel)
}
}
3. To transfer data between screens (where it is really needed) ControllersList turned into an enum with Associated Values:
enum ControllersList {
case textInputScreen
case textConfirmationScreen(text: String)
}
4. In order for the business logic not to affect the navigation, nor the navigation on the business logic, as well as for quick reuse of screens, it was necessary to move the navigation to a separate layer. This is how the Coordinator and the Coordination protocol appeared:
protocol Coordination {
func show(view: ControllersList, firstPosition: Bool)
func popFromCurrentController()
}
final class Coordinator {
private var navigationController = UINavigationController()
private var factory: ControllerBuilder?
private func navigateWithFirstPositionInStack(to: UIViewController) {
navigationController.viewControllers = [to]
}
private func navigate(to: UIViewController) {
navigationController.pushViewController(to, animated: true)
}
}
extension Coordinator: Coordination {
func popFromCurrentController() {
navigationController.popViewController(animated: true)
}
func show(view: ControllersList, firstPosition: Bool) {
guard let controller = factory?.buildController(for: view) else { return }
firstPosition ? navigateWithFirstPositionInStack(to: controller) : navigate(to: controller)
}
}
It is important to note here that the protocol can describe more methods, incl. like the Coordinator, it can implement different protocols, depending on the needs.
5. With all this, I also wanted to limit the set of actions that the developer had to perform by adding a new screen to the application. At the moment, it was necessary to remember that somewhere you need to register dependencies, and it is possible to do some other actions in order for the navigation to work.
6. I didn't want to create additional routers and coordinators at all. Moreover, creating additional logic for navigation could significantly complicate both the perception of navigation and the reuse of screens. All this led to a chain of changes that ultimately looked like this:
//MARK - Dependences with controllers associations
fileprivate extension ControllersList {
typealias scope = AssemblyServices
var assembly: Assembly {
switch self {
case .textInputScreen:
return TextInputAssembly(coordinator: scope.coordinator)
case .textConfirmationScreen(let text):
return TextConfirmationAssembly(coordinator: scope.coordinator, text: text)
}
}
}
//MARK - Services all time in memory
fileprivate enum AssemblyServices {
static let coordinator: oordinationDependencesRegstration = Coordinator()
static let controllerFactory: ControllerBuilderDependencesRegistration = ControllerFacotry()
}
//MARL: - RootAssembly Implementation
final class RootAssembly {
fileprivate typealias scope = AssemblyServices
private func registerPropertyDependences() {
// this place for propery dependences
}
}
// MARK: - AssemblyDataSource implementation
extension RootAssembly: AssemblyDataSource {
func getAssembly(key: ControllersList) -> Assembly? {
return key.assembly
}
}
Now, when creating a new screen, the developer just had to make changes to the ControllersList, and then the compiler itself showed where it was necessary to make changes. Adding new screens to the ControllersList did not affect the current navigation scheme in any way, and the dependency management logic was easy to follow. Also, using ControllersList, you can easily find all points of entry into a particular screen, and it became easy to reuse screens.
Conclusion
This example is a simplified implementation of the idea and does not cover all use cases; nevertheless, the approach itself has shown itself to be quite flexible and adaptive.
The disadvantages of this approach are the following:
- , , . ControllersList NavigationEvents, , ;
- , ;
- , , . , .
Most of the posts about navigation and data transfer in IOS applications affect either the use of coordinators and routers (for each or a group of screens), or navigation through segue, singleton, etc., but none of these options suited me for one or another reasons.
Perhaps this approach will suit you for solving problems, thanks for your time!