Application modularization has a number of benefits and challenges. Good modularization exploits Xcode’s feature of the parallel compilation of project files. When we have a lot of dependencies, like the Firebase module that consumes considerable build time, the compile-time increases dramatically.
With that in mind, modularization aims not only to make the project build independent of Firebase, or any other module, but also to organize the way developers work. Furthermore, it allows the fragmentation of the team by modules.
This article focuses on implementing a framework that allows you to use modularization concepts in applications that use Coordinator. However, this structure can be adapted to other architectures, since the modularization proposal focuses on the screen transition, keeping a simple complexity of implementation and use.
The conception of this article took place after long studies on modularization and how to fit it into the development process in a simple way. In the community, there is no clear discussion on this subject, much less in an objective implementation process.
Some projects mix many other concepts, which makes it difficult to understand the subject. However, inspired by several articles on modularization, this document will detail the process for making any application modularized.
Before going into implementation, the key to understanding the problem is that applications contain specific functions, detailing some information for the user.
Thinking about puzzle pieces, we have a piece for the user login, another piece for the profile, another piece for the shopping cart, and so on. In some beginning articles on modularization, the concept of features that specify the parts of the application is used.
When trying to separate the pieces, in the case of an application that supports the Coordinator concept, the problem of the connection between the screens arises. How will HomeViewController (HomeFeature) communicate with ProfileViewController (ProfileFeature)? Also, how is the presentation of screens in the UINavigationController, or UITabBarController, or in the UIViewController itself? These were the two most difficult questions to understand to get a solution.
Speaking briefly about the Coordinator, they are responsible for transitioning between screens. Each implemented view controller must have access to its coordinator, with the transition rules for the other screens.
Not to go deeper into this subject, which is not the purpose of this article, in HomeCoordinator there is a showProfile function that communicates with the ProfileCoordinator, making the transition defined by it. Good construction of coordinators will facilitate the modularization process.
Swift Package Manager
Swift Package Manager (SPM) is an Apple dependency manager that distributes libraries, frameworks, and modules in repositories with git support. It integrates with Xcode and allows for quick creation with the
swift package init command, as well as coding directly from the IDE when opening Package.swift.
It has a simple configuration file with several important features, especially after Swift 5.3. Reading about this topic is recommended, however, it is not mandatory for this article, as other forms of distribution and modularization such as xcframework or cocoapods can be used.
NotificationCenter is critical to implementing the modularization solution. It communicates between the module (Feature) and the application to make screen transitions.
Other solutions can be explored to carry out this process, but in this article, NotificationCenter and the static shared property were used, which is visible both for the modules and for the application. Abstractions can be made to make the notifications process private, creating another instance of NotificationCenter, in addition to an API of methods focused on this process.
FlowType is a protocol used to send screen transition requests from modules to the application, where each module must implement its Flows, such as HomeFlows (HomeFeature), for example. As shown in Figure 1, the FlowType protocol is detailed without mandatory methods or properties.
It has no dependency on other protocols, such as RawRepresentable, keeping the protocol primitive and able to be used without generic types. However, the objects that will implement it must be enumerators, where each case can be used to send data to the next screens, such as
case profile(User) or
Using this protocol in NotificationCenter can be straightforward using the post and addObservable methods. However, to ensure the integrity and a simpler encoding process, an abstraction is performed to support the protocol. With this, the API for making screen transitions is straightforward.
Figure 2 details the implementation of the two methods post(_ rootViewController: UIViewController, _ flow: FlowType) and track(perform action: @escaping (UIViewController, FlowType) -> Void). The first method is used by the coordinator to submit the transition request to the desired screen, and the second method is used by the application to handle the request and call the corresponding coordinator.
The HomeCoordinator is the example coordinator that contains the methods showProfile(_ user: User) and showLogin for when the user has logged out. The first step is to define HomeFlows, as shown in Figure 3, which has the two necessary cases to allow the transition from HomeViewController to the others. The second step is to use the post(_:_:) method implemented directly in NotificationCenter to send the transition request.
HomeCoordinator uses the UINavigationController to make screen transitions within the module. Normally, the home screen does not have other screens linked in the flow, it is mainly used to start other flows (communicating with other features).
However, in the case of a flow as a profile, there may be several screens inside the module and consequently several coordinators with direct communication between them, without going through the use of FlowType and the transition methods via NotificationCenter.
It is important to emphasize that the methods discussed in this article are only used for communication between modules, mediated by the application.
Figure 4 details the implementation of the HomeCoordinator methods using FlowType. In the case of the profile feature, the ProfileCoordinator uses the UINavigationController to transition between the home screen and the profile screen.
On the other hand, login is done by replacing the current screen with the login screen, which may or may not use the navigation controller. Nevertheless, this decision must be made by the application that will make the transition between screens and, therefore, coordinators always inform the view controller that will be used in the screen transition.
AppDependencies is an object instantiated by the application in AppDelegate or another object that handles application integrity. It has the trackDependencies method which is called in init.
The preference of using static instances or methods must be decided to best fit the project. Using NotificationCenter’s track method, we can convert the types to the desired FlowType and perform the desired transition operations.
Figure 5 implements the trackDependencies method and calls homeFlow(_ rootViewController: UIViewController, _ flow: HomeFlows) so that the responsible coordinators are called. In the case of login, this transition is made by AppDelegate, through the switchToLogin() method.
In the profile, the AppDependencies instance the ProfileCoordinator passing the UINavigationController so that it transitions correctly, as expected. Thus, it is possible to use this solution to perform different types of operations, maintaining the independence of the modules and the application’s responsibility to make the transitions.
The structure discussed in this article allowed the implementation of an application with 20 different features successfully, maintaining the integrity of the application. It covers transitions such as push, present, and more complex ones that involve other parts of the application, especially in SwiftUI, such as an AppState.
In general terms, transitions have lost the intelligence to check which part of the flow the application is in. Despite this being a weak point, no problems were found regarding this.
The API of modules that define visible methods and objects, marked with the public attribute, is also simplified. In modules, only coordinators that can be interacted externally and the FlowType implemented in the module are public.
In most of them, it is possible that the module visibly only has the coordinator, the initialization method, the start, and the flow. Thus, this abstraction has several positive points to consider and can be the beginning for programmers to know and use the concepts of modularization in applications.
Thanks for getting this far!